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
This commit is contained in:
Ben Kwa
2015-12-09 14:33:49 -08:00
parent 743c7c2f0f
commit db65cd5e48
5 changed files with 300 additions and 299 deletions

View File

@@ -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) {

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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));
}
}
}