From 2f0533f19e56fa91f96d0892e084d4effdab8a19 Mon Sep 17 00:00:00 2001 From: Jonas Kalderstam Date: Sat, 15 Oct 2016 21:57:14 +0200 Subject: [PATCH] Multi-select reimplemented --- .../notepad/data/model/sql/Task.java | 28 ++- .../notepad/ui/common/DialogMoveToList.java | 68 ++---- .../notepad/ui/list/ActivityList.java | 20 -- .../notepad/ui/list/ItemViewHolder.java | 9 +- .../notepad/ui/list/SelectedItemHandler.java | 34 +++ .../notepad/ui/list/TaskListFragment.java | 210 +++++++++++------- .../notepad/util/ArrayHelper.java | 12 + 7 files changed, 216 insertions(+), 165 deletions(-) diff --git a/app/src/main/java/com/nononsenseapps/notepad/data/model/sql/Task.java b/app/src/main/java/com/nononsenseapps/notepad/data/model/sql/Task.java index 5cc1804b6..6d060fa34 100644 --- a/app/src/main/java/com/nononsenseapps/notepad/data/model/sql/Task.java +++ b/app/src/main/java/com/nononsenseapps/notepad/data/model/sql/Task.java @@ -217,6 +217,18 @@ private Uri getMoveItemRightUri() { .withAppendedPath(URI_WRITE_MOVEITEMRIGHT, Long.toString(_id)); } + public static int delete(final long id, Context context) { + if (id > 0) { + return context.getContentResolver() + .delete(Uri.withAppendedPath(Uri.withAppendedPath( + Uri.parse(MyContentProvider.SCHEME + MyContentProvider.AUTHORITY), + TABLE_NAME), Long.toString(id)), + null, + null); + } + throw new IllegalArgumentException("Can only delete positive ids"); + } + public static class Columns implements BaseColumns { private Columns() { @@ -346,13 +358,13 @@ private Columns() { .append(" AFTER UPDATE OF ") .append(arrayToCommaString(new String[] { Columns.TITLE, Columns.NOTE })).append(" ON ").append(TABLE_NAME) - + .append(" WHEN old.") .append(Columns.TITLE).append(" IS NOT new.") .append(Columns.TITLE).append(" OR old.") .append(Columns.NOTE).append(" IS NOT new.") .append(Columns.NOTE) - + .append(" BEGIN ").append(HISTORY_TRIGGER_BODY).append(" END;") .toString(); public static final String CREATE_HISTORY_INSERT_TRIGGER = new StringBuilder( @@ -438,7 +450,7 @@ private Columns() { * This is a view which returns the tasks in the specified list with headers * suitable for dates, if any tasks would be sorted under them. Provider * hardcodes the sort order for this. - * + * * if listId is null, will return for all lists */ public static final String CREATE_SECTIONED_DATE_VIEW(final String listId) { @@ -742,7 +754,7 @@ public Long setAsCompleted() { /** * Set first line as title, rest as note. - * + * * @param text */ public void setText(final String text) { @@ -823,7 +835,7 @@ public Task(final ContentValues values) { } } } - + public Task(final JSONObject json) throws JSONException { if (json.has(Columns.TITLE)) this.title = json.getString(Columns.TITLE); @@ -885,7 +897,7 @@ public ContentValues getContent() { * Compares this task to another and returns true if their contents are the * same. Content is defined as: title, note, duedate, completed != null * Returns false if title or note are null. - * + * * The intended usage is the editor where content and not id's or position * are of importance. */ @@ -1129,7 +1141,7 @@ public String getSQLMoveItemRight(final ContentValues values) { .format("UPDATE %1$s SET %2$s = 1, %3$s = 2 WHERE %4$s IS new.%4$s;", TABLE_NAME, Columns.LEFT, Columns.RIGHT, Columns._ID)) - + .append(posUniqueConstraint("new", "Moving list, new positions not unique/ordered")) .append(posUniqueConstraint("old", "Moving list, old positions not unique/ordered")) @@ -1138,7 +1150,7 @@ public String getSQLMoveItemRight(final ContentValues values) { /** * If moving left, then edgeCol is left and vice-versa. Values should come * from getMoveValues - * + * * 1 = table name 2 = left 3 = right 4 = edgecol 5 = old.left 6 = old.right * 7 = target.pos (actually target.edgecol) 8 = dblist 9 = old.dblist */ diff --git a/app/src/main/java/com/nononsenseapps/notepad/ui/common/DialogMoveToList.java b/app/src/main/java/com/nononsenseapps/notepad/ui/common/DialogMoveToList.java index 705703598..42dd9a8d4 100644 --- a/app/src/main/java/com/nononsenseapps/notepad/ui/common/DialogMoveToList.java +++ b/app/src/main/java/com/nononsenseapps/notepad/ui/common/DialogMoveToList.java @@ -39,35 +39,24 @@ import com.nononsenseapps.notepad.data.model.sql.Task; import com.nononsenseapps.notepad.data.model.sql.TaskList; import com.nononsenseapps.notepad.databinding.FragmentDialogMovetolistBinding; +import com.nononsenseapps.notepad.util.ArrayHelper; +import com.nononsenseapps.notepad.util.AsyncTaskHelper; -public class DialogMoveToList extends DialogFragment { +import java.util.Collection; - // public interface EditListDialogListener { - // void onFinishEditDialog(long id); - // } +public class DialogMoveToList extends DialogFragment { static final String TASK_IDS = "task_ids"; - private TaskList mTaskList; - private long[] taskIds = null; private FragmentDialogMovetolistBinding binding; - // private EditListDialogListener listener; - public static DialogMoveToList getInstance(final Long... tasks) { - long[] taskIds = new long[tasks.length]; - for (int i = 0; i < tasks.length; i++) { - taskIds[i] = tasks[i].longValue(); - } - - return getInstance(taskIds); - } - - public static DialogMoveToList getInstance(final long... taskIds) { + public static DialogMoveToList getInstance(final Collection taskIds) { DialogMoveToList dialog = new DialogMoveToList(); Bundle args = new Bundle(); - args.putLongArray(TASK_IDS, taskIds); + // To array fixes threading issues + args.putLongArray(TASK_IDS, ArrayHelper.toArray(taskIds)); dialog.setArguments(args); return dialog; } @@ -165,35 +154,20 @@ public void onLoaderReset(Loader arg0) { } void moveItems(final long toListId, final long[] taskIds) { - // TODO do in background - // for (long id: taskIds) { - // final Cursor c = - // getActivity().getContentResolver().query(Task.getUri(id), - // Task.Columns.FIELDS, null, null, null); - // - // if (c.moveToFirst()) { - // Task t = new Task(c); - // // Remove from old location - // t.delete(getActivity()); - // // Reset, and set new list - // t.resetForInsertion(); - // t.dblist = toListId; - // // And save anew - // t.save(getActivity()); - // } - // - // c.close(); - // } - - final ContentValues val = new ContentValues(); - val.put(Task.Columns.DBLIST, toListId); - - // where _ID in (1, 2, 3) - final String whereId = new StringBuilder(Task.Columns._ID) - .append(" IN (").append(DAO.arrayToCommaString(taskIds)) - .append(")").toString(); - - getActivity().getContentResolver().update(Task.URI, val, whereId, null); + final ContentValues val = new ContentValues(); + val.put(Task.Columns.DBLIST, toListId); + + // where _ID in (1, 2, 3) + final String whereId = new StringBuilder(Task.Columns._ID) + .append(" IN (").append(DAO.arrayToCommaString(taskIds)) + .append(")").toString(); + + AsyncTaskHelper.background(new AsyncTaskHelper.Job() { + @Override + public void doInBackground() { + getActivity().getContentResolver().update(Task.URI, val, whereId, null); + } + }); } void okClicked() { diff --git a/app/src/main/java/com/nononsenseapps/notepad/ui/list/ActivityList.java b/app/src/main/java/com/nononsenseapps/notepad/ui/list/ActivityList.java index 36cef107f..1d7f58968 100644 --- a/app/src/main/java/com/nononsenseapps/notepad/ui/list/ActivityList.java +++ b/app/src/main/java/com/nononsenseapps/notepad/ui/list/ActivityList.java @@ -244,26 +244,6 @@ public void openTask(final Uri taskUri, final long listId, final View origin) { //} } - /** - * Show a snackbar indicating that items were deleted, together with an undo button. - */ - @Override - public void deleteTasksWithUndo(Snackbar.Callback dismissCallback, - final View.OnClickListener listener, final Task... tasks) { - CharSequence text; - try { - text = getResources().getQuantityString(R.plurals.notedeleted_msg, tasks.length, - tasks.length); - } catch (Exception e) { - // Protect against faulty translations - text = getResources().getString(R.string.deleted); - } - Snackbar.make(mFab, text, Snackbar.LENGTH_LONG) - .setAction(R.string.undo, listener) - .setCallback(dismissCallback) - .show(); - } - public void addTask() { addTaskInList("", ListHelper.getARealList(this, mCurrentList)); } diff --git a/app/src/main/java/com/nononsenseapps/notepad/ui/list/ItemViewHolder.java b/app/src/main/java/com/nononsenseapps/notepad/ui/list/ItemViewHolder.java index d821da43e..6166f7906 100644 --- a/app/src/main/java/com/nononsenseapps/notepad/ui/list/ItemViewHolder.java +++ b/app/src/main/java/com/nononsenseapps/notepad/ui/list/ItemViewHolder.java @@ -103,6 +103,11 @@ public void onBind(final Cursor cursor) { @Override public void onClick(final View v) { + if (selectedItemHandler.isActiveSelectionMode()) { + // same as long press + onLongClick(v); + return; + } if (taskListFragment.getListener() != null && id > 0) { taskListFragment.getListener().openTask(Task.getUri(id), listId, v); } @@ -113,10 +118,6 @@ public boolean onLongClick(final View v) { if (id < 1) { return false; } - // TODO - //listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL); - // Also select the item in question - //listView.setItemChecked(pos, true); selectedItemHandler.toggleSelection(id); binding.getRoot().setActivated(selectedItemHandler.isItemSelected(id)); diff --git a/app/src/main/java/com/nononsenseapps/notepad/ui/list/SelectedItemHandler.java b/app/src/main/java/com/nononsenseapps/notepad/ui/list/SelectedItemHandler.java index 62909995e..f2260d433 100644 --- a/app/src/main/java/com/nononsenseapps/notepad/ui/list/SelectedItemHandler.java +++ b/app/src/main/java/com/nononsenseapps/notepad/ui/list/SelectedItemHandler.java @@ -1,24 +1,58 @@ package com.nononsenseapps.notepad.ui.list; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.view.ActionMode; + import java.util.Set; import java.util.TreeSet; public class SelectedItemHandler { private final TreeSet selected = new TreeSet<>(); + private final AppCompatActivity activity; + private final ActionMode.Callback actionModeCallback; + private ActionMode actionMode = null; + + public SelectedItemHandler(AppCompatActivity activity, ActionMode.Callback actionModeCallback) { + this.activity = activity; + this.actionModeCallback = actionModeCallback; + } public boolean isItemSelected(long id) { return selected.contains(id); } public void toggleSelection(long id) { + if (actionMode == null) { + actionMode = activity.startSupportActionMode(actionModeCallback); + } + if (selected.contains(id)) { selected.remove(id); } else { selected.add(id); } + + if (selected.isEmpty()) { + actionMode.finish(); + } else { + actionMode.setTitle(String.valueOf(selected.size())); + actionMode.invalidate(); + } } public Set getSelected() { return selected; } + + public void clear() { + selected.clear(); + if (actionMode != null) { + actionMode.finish(); + actionMode = null; + } + } + + public boolean isActiveSelectionMode() { + return actionMode != null; + } } diff --git a/app/src/main/java/com/nononsenseapps/notepad/ui/list/TaskListFragment.java b/app/src/main/java/com/nononsenseapps/notepad/ui/list/TaskListFragment.java index 21800356d..19e654060 100644 --- a/app/src/main/java/com/nononsenseapps/notepad/ui/list/TaskListFragment.java +++ b/app/src/main/java/com/nononsenseapps/notepad/ui/list/TaskListFragment.java @@ -19,6 +19,7 @@ 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; @@ -26,18 +27,18 @@ import android.os.Bundle; import android.preference.PreferenceManager; import android.support.annotation.Nullable; -import android.support.design.widget.Snackbar; 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.ActionMode; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -46,22 +47,28 @@ 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.Map; +import java.util.Collection; +import java.util.Set; +import java.util.TreeSet; public class TaskListFragment extends Fragment - implements OnSharedPreferenceChangeListener, SyncStatusMonitor.OnSyncStartStopListener { + 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"; @@ -92,9 +99,8 @@ public class TaskListFragment extends Fragment private ActionMode mMode; private SwipeRefreshLayout mSwipeRefreshLayout; - private boolean mDeleteWasUndone = false; private ItemTouchHelper touchHelper; - private final SelectedItemHandler selectedItemHandler = new SelectedItemHandler(); + private SelectedItemHandler selectedItemHandler; public TaskListFragment() { super(); @@ -253,56 +259,30 @@ Intent getShareIntent() { /** * Delete tasks and display a snackbar with an undo action * - * @param taskMap */ - private void deleteTasks(final Map taskMap) { - final Task[] tasks = taskMap.values().toArray(new Task[taskMap.size()]); - + private void deleteTasks(final Set orgItemIds) { // If any are locked, ask for password first final boolean locked = SharedPreferencesHelper.isPasswordSet(getActivity()); - // Reset undo flag - mDeleteWasUndone = false; - - // Dismiss callback - final Snackbar.Callback dismissCallback = new Snackbar.Callback() { - @Override - public void onDismissed(Snackbar snackbar, int event) { - // Do nothing if dismissed because action was pressed - // Dismiss wil be called more than once if undo is pressed - if (Snackbar.Callback.DISMISS_EVENT_ACTION != event && !mDeleteWasUndone) { - // Delete them - AsyncTaskHelper.background(new AsyncTaskHelper.Job() { - @Override - public void doInBackground() { - for (Task t : tasks) { - try { - t.delete(getActivity()); - } catch (Exception ignored) { - } - } - } - }); - } - } - }; - - // Undo callback - final View.OnClickListener undoListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - mDeleteWasUndone = true; - // Returns removed items to view - // TODO jonas - // mAdapter.reset(); - } - }; + // copy to avoid threading issues + final Set itemIds = new TreeSet<>(); + itemIds.addAll(orgItemIds); final PasswordConfirmedListener pListener = new PasswordConfirmedListener() { @Override public void onPasswordConfirmed() { - removeTasksFromList(tasks); - mListener.deleteTasksWithUndo(dismissCallback, undoListener, tasks); + 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()); + } + } + } + }); } }; @@ -312,15 +292,8 @@ public void onPasswordConfirmed() { delpf.show(getFragmentManager(), "multi_delete_verify"); } else { // Just run it directly - removeTasksFromList(tasks); - mListener.deleteTasksWithUndo(dismissCallback, undoListener, tasks); - } - } - - private void removeTasksFromList(Task... tasks) { - for (Task task : tasks) { - // TODO jonas - // mAdapter.remove(mAdapter.getItemPosition(task._id)); + Log.d(TAG, "deleteTasks: run it"); + pListener.onPasswordConfirmed(); } } @@ -368,6 +341,8 @@ public void onCreate(Bundle savedState) { setHasOptionsMenu(true); + selectedItemHandler = new SelectedItemHandler((AppCompatActivity) getActivity(), this); + syncStatusReceiver = new SyncStatusMonitor(); if (getArguments().getLong(LIST_ID, -1) == -1) { @@ -435,15 +410,6 @@ public void onActivityCreated(final Bundle state) { // Get the global list settings final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); - // Load pref for item height - //mRowCount = prefs.getInt(getString(R.string.key_pref_item_max_height), 3); - //mHideCheckbox = prefs.getBoolean(getString(R.string.pref_hidecheckboxes), false); - - // mSortType = prefs.getString(getString(R.string.pref_sorttype), - // getString(R.string.default_sorttype)); - // mListType = prefs.getString(getString(R.string.pref_listtype), - // getString(R.string.default_listtype)); - mCallback = new LoaderCallbacks() { @Override public Loader onCreateLoader(int id, Bundle arg1) { @@ -497,12 +463,6 @@ public Loader onCreateLoader(int id, Bundle arg1) { @Override public void onLoadFinished(Loader loader, Cursor c) { if (loader.getId() == LOADER_TASKS) { - Log.d(TAG, - "onLoadFinished() called"); - for (int i = 0; i < c.getCount(); i++) { - c.moveToPosition(i); - Log.d(TAG, "onLoadFinished " + i + ": " + c.getLong(0)); - } mAdapter.swapCursor(c); } } @@ -608,14 +568,6 @@ public void onSharedPreferenceChanged(final SharedPreferences prefs, final Strin mSortType = null; reload = true; } - /* else if (key.equals(getString(R.string.key_pref_item_max_height))) { - mRowCount = prefs.getInt(key, 3); - reload = true; - } */ - /*else if (key.equals(getString(R.string.pref_hidecheckboxes))) { - mHideCheckbox = prefs.getBoolean(key, false); - reload = true; - }*/ if (reload && mCallback != null) { getLoaderManager().restartLoader(LOADER_TASKS, null, mCallback); @@ -658,6 +610,98 @@ 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 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 @@ -665,12 +709,6 @@ public long getListId() { */ public interface TaskListCallbacks { void openTask(final Uri uri, final long listId, final View origin); - - /** - * Show a snackbar indicating that items were deleted, together with an undo button. - */ - void deleteTasksWithUndo(Snackbar.Callback dismissCallback, View.OnClickListener listener, - Task... tasks); } class DragHandler extends ItemTouchHelper.Callback { diff --git a/app/src/main/java/com/nononsenseapps/notepad/util/ArrayHelper.java b/app/src/main/java/com/nononsenseapps/notepad/util/ArrayHelper.java index 0148ffe73..285cd39d8 100644 --- a/app/src/main/java/com/nononsenseapps/notepad/util/ArrayHelper.java +++ b/app/src/main/java/com/nononsenseapps/notepad/util/ArrayHelper.java @@ -17,9 +17,21 @@ package com.nononsenseapps.notepad.util; +import java.util.Collection; +import java.util.Iterator; + public class ArrayHelper { public static T[] toArray(T... items) { return items; } + + public static long[] toArray(Collection longs) { + Iterator it = longs.iterator(); + long[] result = new long[longs.size()]; + for (int i = 0; i < result.length; i++) { + result[i] = it.next(); + } + return result; + } }