diff options
| author | 2015-12-09 14:33:49 -0800 | |
|---|---|---|
| committer | 2015-12-14 11:47:27 -0800 | |
| commit | db65cd5e48e847e99cc64bf79a2fb7fe3bbfd31a (patch) | |
| tree | 481f69a245d803741123373af824852e23400279 | |
| parent | 743c7c2f0f57b4cef7cc11154b2e804a7eb33177 (diff) | |
Fix file deletion after the move to Model IDs.
- Add methods to the DocumentsAdatper to hide and unhide files. This
removes that burden from the Model.
- Remove clunky markForDeletion/finalizeDeletion and all related code
from the Model. Replace with a straight-up delete method.
- Modify deletion code in the DirectoryFragment. Deletion now looks
like:
- user presses delete
- DocumentsAdapter hides the deleted files
- If the user presses cancel, the DocumentsAdapter unhides the files.
- If the user doesn't cancel, Model.delete is called to delete the
files.
- Fix deletion-related Model tests.
BUG=26024369
Change-Id: I02e2131c1aff1ebcd0bdb93d374675fd157d7f51
5 files changed, 301 insertions, 300 deletions
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java index 107ade891f73..b66a12441bf2 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java +++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java @@ -116,10 +116,12 @@ import com.android.documentsui.model.DocumentInfo; import com.android.documentsui.model.DocumentStack; import com.android.documentsui.model.RootInfo; import com.google.common.collect.Lists; +import com.google.common.collect.Sets; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Set; /** * Display the documents inside a single directory. @@ -844,8 +846,11 @@ public class DirectoryFragment extends Fragment { Context context = getActivity(); String message = Shared.getQuantityString(context, R.plurals.deleting, selected.size()); - mModel.markForDeletion(selected); + // Hide the files in the UI. + final SparseArray<String> toDelete = mAdapter.hide(selected.getAll()); + // Show a snackbar informing the user that files will be deleted, and give them an option to + // cancel. final Activity activity = getActivity(); Snackbars.makeSnackbar(activity, message, Snackbar.LENGTH_LONG) .setAction( @@ -859,19 +864,22 @@ public class DirectoryFragment extends Fragment { @Override public void onDismissed(Snackbar snackbar, int event) { if (event == Snackbar.Callback.DISMISS_EVENT_ACTION) { - mModel.undoDeletion(); + // If the delete was cancelled, just unhide the files. + mAdapter.unhide(toDelete); } else { - mModel.finalizeDeletion( + // Actually kick off the delete. + mModel.delete( + selected, new Model.DeletionListener() { @Override - public void onError() { - Snackbars.makeSnackbar( - activity, - R.string.toast_failed_delete, - Snackbar.LENGTH_LONG) - .show(); - - } + public void onError() { + Snackbars.makeSnackbar( + activity, + R.string.toast_failed_delete, + Snackbar.LENGTH_LONG) + .show(); + + } }); } } @@ -1049,7 +1057,6 @@ public class DirectoryFragment extends Fragment { @Override public void onBindViewHolder(DocumentHolder holder, int position) { - final Context context = getContext(); final State state = getDisplayState(); final RootsCache roots = DocumentsApplication.getRootsCache(context); @@ -1239,6 +1246,45 @@ public class DirectoryFragment extends Fragment { return mModelIds.get(adapterPosition); } + /** + * Hides a set of items from the associated RecyclerView. + * + * @param ids The Model IDs of the items to hide. + * @return A SparseArray that maps the hidden IDs to their old positions. This can be used + * to {@link #unhide} the items if necessary. + */ + public SparseArray<String> hide(String... ids) { + Set<String> toHide = Sets.newHashSet(ids); + + // Proceed backwards through the list of items, because each removal causes the + // positions of all subsequent items to change. + SparseArray<String> hiddenItems = new SparseArray<>(); + for (int i = mModelIds.size() - 1; i >= 0; --i) { + String id = mModelIds.get(i); + if (toHide.contains(id)) { + hiddenItems.put(i, mModelIds.remove(i)); + notifyItemRemoved(i); + } + } + + return hiddenItems; + } + + /** + * Unhides a set of previously hidden items. + * + * @param ids A sparse array of IDs from a previous call to {@link #hide}. + */ + public void unhide(SparseArray<String> ids) { + // Proceed backwards through the list of items, because each addition causes the + // positions of all subsequent items to change. + for (int i = ids.size() - 1; i >= 0; --i) { + int pos = ids.keyAt(i); + String id = ids.get(pos); + mModelIds.add(pos, id); + notifyItemInserted(pos); + } + } } private static String formatTime(Context context, long when) { diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/Model.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/Model.java index 49691d416b22..fce16c63979b 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/Model.java +++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/Model.java @@ -19,7 +19,6 @@ package com.android.documentsui.dirlist; import static com.android.documentsui.Shared.DEBUG; import static com.android.documentsui.model.DocumentInfo.getCursorString; import static com.android.internal.util.Preconditions.checkNotNull; -import static com.android.internal.util.Preconditions.checkState; import android.content.ContentProviderClient; import android.content.ContentResolver; @@ -34,7 +33,6 @@ import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.support.v7.widget.RecyclerView; import android.util.Log; -import android.util.SparseArray; import com.android.documentsui.BaseActivity.DocumentContext; import com.android.documentsui.DirectoryResult; @@ -42,11 +40,9 @@ import com.android.documentsui.DocumentsApplication; import com.android.documentsui.RootCursorWrapper; import com.android.documentsui.dirlist.MultiSelectManager.Selection; import com.android.documentsui.model.DocumentInfo; -import com.android.internal.annotations.GuardedBy; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Set; @@ -56,14 +52,9 @@ import java.util.Set; @VisibleForTesting public class Model implements DocumentContext { private static final String TAG = "Model"; - private RecyclerView.Adapter<?> mViewAdapter; private Context mContext; private int mCursorCount; private boolean mIsLoading; - @GuardedBy("mPendingDelete") - private Boolean mPendingDelete = false; - @GuardedBy("mPendingDelete") - private Set<String> mMarkedForDeletion = new HashSet<>(); private List<UpdateListener> mUpdateListeners = new ArrayList<>(); @Nullable private Cursor mCursor; @Nullable String info; @@ -72,7 +63,6 @@ public class Model implements DocumentContext { Model(Context context, RecyclerView.Adapter<?> viewAdapter) { mContext = context; - mViewAdapter = viewAdapter; } /** @@ -142,9 +132,7 @@ public class Model implements DocumentContext { @VisibleForTesting int getItemCount() { - synchronized(mPendingDelete) { - return mCursorCount - mMarkedForDeletion.size(); - } + return mCursorCount; } /** @@ -197,107 +185,24 @@ public class Model implements DocumentContext { return mCursor; } - List<DocumentInfo> getDocumentsMarkedForDeletion() { - // TODO(stable-id): This could be just a plain old selection now. - synchronized (mPendingDelete) { - final int size = mMarkedForDeletion.size(); - List<DocumentInfo> docs = new ArrayList<>(size); - - for (String id: mMarkedForDeletion) { - Integer position = mPositions.get(id); - checkState(position != null); - mCursor.moveToPosition(position); - final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(mCursor); - docs.add(doc); - } - return docs; - } - } - - /** - * Marks the given files for deletion. This will remove them from the UI. Clients must then - * call either {@link #undoDeletion()} or {@link #finalizeDeletion()} to cancel or confirm - * the deletion, respectively. Only one deletion operation is allowed at a time. - * - * @param selected A selection representing the files to delete. - */ - void markForDeletion(Selection selected) { - synchronized (mPendingDelete) { - mPendingDelete = true; - // Only one deletion operation at a time. - checkState(mMarkedForDeletion.size() == 0); - // There should never be more to delete than what exists. - checkState(mCursorCount >= selected.size()); - - // Adapter notifications must be sent in reverse order of adapter position. This is - // because each removal causes subsequent item adapter positions to change. - SparseArray<String> ids = new SparseArray<>(); - for (int i = ids.size() - 1; i >= 0; i--) { - int pos = ids.keyAt(i); - mMarkedForDeletion.add(ids.get(pos)); - mViewAdapter.notifyItemRemoved(pos); - if (DEBUG) Log.d(TAG, "Scheduled " + pos + " for delete."); - } - } - } - - /** - * Cancels an ongoing deletion operation. All files currently marked for deletion will be - * unmarked, and restored in the UI. See {@link #markForDeletion(Selection)}. - */ - void undoDeletion() { - synchronized (mPendingDelete) { - // Iterate over deleted items, temporarily marking them false in the deletion list, and - // re-adding them to the UI. - for (String id: mMarkedForDeletion) { - Integer pos= mPositions.get(id); - checkNotNull(pos); - mMarkedForDeletion.remove(id); - mViewAdapter.notifyItemInserted(pos); - } - resetDeleteData(); - } - } - - private void resetDeleteData() { - synchronized (mPendingDelete) { - mPendingDelete = false; - mMarkedForDeletion.clear(); - } - } - - /** - * Finalizes an ongoing deletion operation. All files currently marked for deletion will be - * deleted. See {@link #markForDeletion(Selection)}. - * - * @param view The view which will be used to interact with the user (e.g. surfacing - * snackbars) for errors, info, etc. - */ - void finalizeDeletion(DeletionListener listener) { - synchronized (mPendingDelete) { - if (mPendingDelete) { - // Necessary to avoid b/25072545. Even when that's resolved, this - // is a nice safe thing to day. - mPendingDelete = false; - final ContentResolver resolver = mContext.getContentResolver(); - DeleteFilesTask task = new DeleteFilesTask(resolver, listener); - task.execute(); - } - } + public void delete(Selection selected, DeletionListener listener) { + final ContentResolver resolver = mContext.getContentResolver(); + new DeleteFilesTask(resolver, listener).execute(selected); } /** * A Task which collects the DocumentInfo for documents that have been marked for deletion, * and actually deletes them. */ - private class DeleteFilesTask extends AsyncTask<Void, Void, List<DocumentInfo>> { + private class DeleteFilesTask extends AsyncTask<Selection, Void, Void> { private ContentResolver mResolver; private DeletionListener mListener; + private boolean mHadTrouble; /** * @param resolver A ContentResolver for performing the actual file deletions. * @param errorCallback A Runnable that is executed in the event that one or more errors - * occured while copying files. Execution will occur on the UI thread. + * occurred while copying files. Execution will occur on the UI thread. */ public DeleteFilesTask(ContentResolver resolver, DeletionListener listener) { mResolver = resolver; @@ -305,17 +210,14 @@ public class Model implements DocumentContext { } @Override - protected List<DocumentInfo> doInBackground(Void... params) { - return getDocumentsMarkedForDeletion(); - } + protected Void doInBackground(Selection... selected) { + List<DocumentInfo> toDelete = getDocuments(selected[0]); + mHadTrouble = false; - @Override - protected void onPostExecute(List<DocumentInfo> docs) { - boolean hadTrouble = false; - for (DocumentInfo doc : docs) { + for (DocumentInfo doc : toDelete) { if (!doc.isDeleteSupported()) { Log.w(TAG, doc + " could not be deleted. Skipping..."); - hadTrouble = true; + mHadTrouble = true; continue; } @@ -326,21 +228,25 @@ public class Model implements DocumentContext { mResolver, doc.derivedUri.getAuthority()); DocumentsContract.deleteDocument(client, doc.derivedUri); } catch (Exception e) { - Log.w(TAG, "Failed to delete " + doc); - hadTrouble = true; + Log.w(TAG, "Failed to delete " + doc, e); + mHadTrouble = true; } finally { ContentProviderClient.releaseQuietly(client); } } - if (hadTrouble) { + return null; + } + + @Override + protected void onPostExecute(Void _) { + if (mHadTrouble) { // TODO show which files failed? b/23720103 mListener.onError(); if (DEBUG) Log.d(TAG, "Deletion task completed. Some deletions failed."); } else { if (DEBUG) Log.d(TAG, "Deletion task completed successfully."); } - resetDeleteData(); mListener.onCompletion(); } diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java index b0bcbcd715af..83c82947626f 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java +++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java @@ -140,10 +140,21 @@ public final class MultiSelectManager implements View.OnKeyListener { mEnvironment.registerDataObserver( new RecyclerView.AdapterDataObserver() { + private List<String> mModelIds = new ArrayList<>(); + @Override public void onChanged() { // TODO(stable-id): This is causing b/22765812 mSelection.clear(); + + // TODO(stable-id): Improve this. It's currently super-inefficient, + // performing a bunch of lookups and inserting into a List. Maybe just add + // another method to the SelectionEnvironment to just grab the whole list at + // once. + mModelIds.clear(); + for (int i = 0; i < mEnvironment.getItemCount(); ++i) { + mModelIds.add(mEnvironment.getModelIdFromAdapterPosition(i)); + } } @Override @@ -165,8 +176,9 @@ public final class MultiSelectManager implements View.OnKeyListener { int endPosition = startPosition + itemCount; // Remove any disappeared IDs from the selection. for (int i = startPosition; i < endPosition; i++) { - String id = mEnvironment.getModelIdFromAdapterPosition(i); + String id = mModelIds.get(i); mSelection.remove(id); + mModelIds.remove(i); } } diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/DirectoryFragmentModelTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/DirectoryFragmentModelTest.java deleted file mode 100644 index ed123b4b26c0..000000000000 --- a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/DirectoryFragmentModelTest.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.documentsui.dirlist; - -import android.content.ContentResolver; -import android.content.Context; -import android.content.ContextWrapper; -import android.database.Cursor; -import android.database.MatrixCursor; -import android.provider.DocumentsContract.Document; -import android.support.v7.widget.RecyclerView; -import android.test.AndroidTestCase; -import android.test.mock.MockContentResolver; -import android.test.suitebuilder.annotation.SmallTest; -import android.view.ViewGroup; - -import com.android.documentsui.DirectoryResult; -import com.android.documentsui.RootCursorWrapper; -import com.android.documentsui.model.DocumentInfo; - -import java.util.ArrayList; -import java.util.List; - -@SmallTest -public class DirectoryFragmentModelTest extends AndroidTestCase { - - // Item count must be an even number (see setUp below) - private static final int ITEM_COUNT = 10; - private static final String[] COLUMNS = new String[]{ - RootCursorWrapper.COLUMN_AUTHORITY, - Document.COLUMN_DOCUMENT_ID - }; - private static Cursor cursor; - - private Context mContext; - private Model model; - - public void setUp() { - setupTestContext(); - - // Make two sets of documents under two different authorities but with identical document - // IDs. - MatrixCursor c = new MatrixCursor(COLUMNS); - for (int i = 0; i < ITEM_COUNT/2; ++i) { - MatrixCursor.RowBuilder row0 = c.newRow(); - row0.add(RootCursorWrapper.COLUMN_AUTHORITY, "authority0"); - row0.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i)); - - MatrixCursor.RowBuilder row1 = c.newRow(); - row1.add(RootCursorWrapper.COLUMN_AUTHORITY, "authority1"); - row1.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i)); -} - cursor = c; - - DirectoryResult r = new DirectoryResult(); - r.cursor = cursor; - - // Instantiate the model with a dummy view adapter and listener that (for now) do nothing. - model = new Model(mContext, new DummyAdapter()); - model.addUpdateListener(new DummyListener()); - model.update(r); - } - - // Tests that the item count is correct. - public void testItemCount() { - assertEquals(ITEM_COUNT, model.getItemCount()); - } - - // Tests that the item count is correct after a deletion. - public void testItemCount_WithDeletion() { - // Simulate deleting 2 files. - delete(2, 4); - - assertEquals(ITEM_COUNT - 2, model.getItemCount()); - } - - // Tests that the item count is correct after a deletion is undone. - public void testItemCount_WithUndoneDeletion() { - // Simulate deleting 2 files. - delete(0, 3); - - // Undo the deletion - model.undoDeletion(); - assertEquals(ITEM_COUNT, model.getItemCount()); - } - - // Tests that the right things are marked for deletion. - public void testMarkForDeletion() { - delete(1, 3); - - List<DocumentInfo> docs = model.getDocumentsMarkedForDeletion(); - assertEquals(2, docs.size()); - assertEquals("1", docs.get(0).documentId); - assertEquals("3", docs.get(1).documentId); - } - - // Tests the base case for Model.getItem. - public void testGetItem() { - for (int i = 0; i < ITEM_COUNT; ++i) { - cursor.moveToPosition(i); - Cursor c = model.getItem(Model.createId(cursor)); - assertEquals(i, c.getPosition()); - } - } - - // Tests that Model.getItem returns the right items after a deletion. - public void testGetItem_WithDeletion() { - // Simulate deleting 2 files. - delete(2, 3); - - List<DocumentInfo> docs = getDocumentInfo(0, 1, 2); - assertEquals("0", docs.get(0).documentId); - assertEquals("1", docs.get(1).documentId); - assertEquals("4", docs.get(2).documentId); - } - - // Tests that Model.getItem returns the right items after a deletion is undone. - public void testGetItem_WithCancelledDeletion() { - delete(0, 1); - model.undoDeletion(); - - // Test that all documents are accounted for, in the right position. - for (int i = 0; i < ITEM_COUNT; ++i) { - assertEquals(Integer.toString(i), getDocumentInfo(i).get(0).documentId); - } - } - - private void setupTestContext() { - final MockContentResolver resolver = new MockContentResolver(); - mContext = new ContextWrapper(getContext()) { - @Override - public ContentResolver getContentResolver() { - return resolver; - } - }; - } - - private void delete(int... positions) { -// model.markForDeletion(new Selection(positions)); - } - - private List<DocumentInfo> getDocumentInfo(int... positions) { -// return model.getDocuments(new Selection(positions)); - return new ArrayList<>(); - } - - private static class DummyListener implements Model.UpdateListener { - public void onModelUpdate(Model model) {} - public void onModelUpdateFailed(Exception e) {} - } - - private static class DummyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { - public int getItemCount() { return 0; } - public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {} - public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - return null; - } - } -} diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/ModelTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/ModelTest.java new file mode 100644 index 000000000000..96ca0f9b262d --- /dev/null +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/ModelTest.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.documentsui.dirlist; + +import static android.test.MoreAsserts.assertNotEqual; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.ContextWrapper; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.os.Bundle; +import android.provider.DocumentsContract; +import android.provider.DocumentsContract.Document; +import android.support.v7.widget.RecyclerView; +import android.test.AndroidTestCase; +import android.test.mock.MockContentProvider; +import android.test.mock.MockContentResolver; +import android.test.suitebuilder.annotation.SmallTest; +import android.view.ViewGroup; + +import com.android.documentsui.DirectoryResult; +import com.android.documentsui.RootCursorWrapper; +import com.android.documentsui.dirlist.MultiSelectManager.Selection; +import com.android.documentsui.model.DocumentInfo; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +@SmallTest +public class ModelTest extends AndroidTestCase { + + private static final int ITEM_COUNT = 10; + private static final String AUTHORITY = "test_authority"; + private static final String[] COLUMNS = new String[]{ + RootCursorWrapper.COLUMN_AUTHORITY, + Document.COLUMN_DOCUMENT_ID, + Document.COLUMN_FLAGS + }; + private static Cursor cursor; + + private Context context; + private Model model; + private TestContentProvider provider; + + public void setUp() { + setupTestContext(); + + MatrixCursor c = new MatrixCursor(COLUMNS); + for (int i = 0; i < ITEM_COUNT; ++i) { + MatrixCursor.RowBuilder row = c.newRow(); + row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY); + row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i)); + row.add(Document.COLUMN_FLAGS, Document.FLAG_SUPPORTS_DELETE); + } + cursor = c; + + DirectoryResult r = new DirectoryResult(); + r.cursor = cursor; + + // Instantiate the model with a dummy view adapter and listener that (for now) do nothing. + model = new Model(context, new DummyAdapter()); + model.addUpdateListener(new DummyListener()); + model.update(r); + } + + // Tests that the item count is correct. + public void testItemCount() { + assertEquals(ITEM_COUNT, model.getItemCount()); + } + + // Tests multiple authorities with clashing document IDs. + public void testModelIdIsUnique() { + MatrixCursor c0 = new MatrixCursor(COLUMNS); + MatrixCursor c1 = new MatrixCursor(COLUMNS); + + + // Make two sets of items with the same IDs, under different authorities. + final String AUTHORITY0 = "auth0"; + final String AUTHORITY1 = "auth1"; + for (int i = 0; i < ITEM_COUNT; ++i) { + MatrixCursor.RowBuilder row0 = c0.newRow(); + row0.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY0); + row0.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i)); + + MatrixCursor.RowBuilder row1 = c1.newRow(); + row1.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY1); + row1.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i)); + } + + for (int i = 0; i < ITEM_COUNT; ++i) { + c0.moveToPosition(i); + c1.moveToPosition(i); + assertNotEqual(Model.createId(c0), Model.createId(c1)); + } + } + + // Tests the base case for Model.getItem. + public void testGetItem() { + for (int i = 0; i < ITEM_COUNT; ++i) { + cursor.moveToPosition(i); + Cursor c = model.getItem(Model.createId(cursor)); + assertEquals(i, c.getPosition()); + } + } + + // Tests that Model.delete works correctly. + public void testDelete() throws Exception { + // Simulate deleting 2 files. + List<DocumentInfo> docsBefore = getDocumentInfo(2, 3); + delete(2, 3); + + provider.assertWasDeleted(docsBefore.get(0)); + provider.assertWasDeleted(docsBefore.get(1)); + } + + private void setupTestContext() { + final MockContentResolver resolver = new MockContentResolver(); + context = new ContextWrapper(getContext()) { + @Override + public ContentResolver getContentResolver() { + return resolver; + } + }; + provider = new TestContentProvider(); + resolver.addProvider(AUTHORITY, provider); + } + + private Selection positionToSelection(int... positions) { + Selection s = new Selection(); + // Construct a selection of the given positions. + for (int p: positions) { + cursor.moveToPosition(p); + s.add(Model.createId(cursor)); + } + return s; + } + + private void delete(int... positions) throws InterruptedException { + Selection s = positionToSelection(positions); + final CountDownLatch latch = new CountDownLatch(1); + + model.delete( + s, + new Model.DeletionListener() { + @Override + public void onError() { + latch.countDown(); + } + @Override + void onCompletion() { + latch.countDown(); + } + }); + latch.await(); + } + + private List<DocumentInfo> getDocumentInfo(int... positions) { + return model.getDocuments(positionToSelection(positions)); + } + + private static class DummyListener implements Model.UpdateListener { + public void onModelUpdate(Model model) {} + public void onModelUpdateFailed(Exception e) {} + } + + private static class DummyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { + public int getItemCount() { return 0; } + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {} + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return null; + } + } + + private static class TestContentProvider extends MockContentProvider { + List<Uri> mDeleted = new ArrayList<>(); + + @Override + public Bundle call(String method, String arg, Bundle extras) { + // Intercept and log delete method calls. + if (DocumentsContract.METHOD_DELETE_DOCUMENT.equals(method)) { + final Uri documentUri = extras.getParcelable(DocumentsContract.EXTRA_URI); + mDeleted.add(documentUri); + return new Bundle(); + } else { + return super.call(method, arg, extras); + } + } + + public void assertWasDeleted(DocumentInfo doc) { + assertTrue(mDeleted.contains(doc.derivedUri)); + } + } +} |