summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Ben Kwa <kenobi@google.com> 2015-09-01 11:03:01 -0700
committer Ben Kwa <kenobi@google.com> 2015-09-08 09:58:35 -0700
commit18fce3cd3ccef83b3a26e34b1eb2161e0e957118 (patch)
treefa636ecc87adb7fddb9716a2350171f291d2b0b4
parent55a309f8e2a972a2f0ef0cd86736d3c2f47a75f6 (diff)
Add unit tests for DirectoryFragment.Model.
Refactor DirectoryFragment.Model to be a static class. Introduce some unit tests. BUG=23754695 Change-Id: Iaa064292ab26b23ac7247e49c05ba91033d84a18
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java137
-rw-r--r--packages/DocumentsUI/tests/src/com/android/documentsui/DirectoryFragmentModelTest.java157
2 files changed, 258 insertions, 36 deletions
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
index edf829d178c9..93921dd51490 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
@@ -62,6 +62,7 @@ import android.os.SystemProperties;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
import android.support.design.widget.Snackbar;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
@@ -132,7 +133,7 @@ public class DirectoryFragment extends Fragment {
private static final String EXTRA_QUERY = "query";
private static final String EXTRA_IGNORE_STATE = "ignoreState";
- private final Model mModel = new Model();
+ private Model mModel;
private final Handler mHandler = new Handler(Looper.getMainLooper());
@@ -304,7 +305,10 @@ public class DirectoryFragment extends Fragment {
? MultiSelectManager.MODE_MULTIPLE
: MultiSelectManager.MODE_SINGLE);
selMgr.addCallback(new SelectionModeListener());
+
+ mModel = new Model(context, selMgr);
mModel.setSelectionManager(selMgr);
+ mModel.addUpdateListener(mAdapter);
mType = getArguments().getInt(EXTRA_TYPE);
mStateKey = buildStateKey(root, doc);
@@ -374,9 +378,7 @@ public class DirectoryFragment extends Fragment {
if (!isAdded()) return;
- // TODO: make the adapter listen to the model
mModel.update(result);
- mAdapter.update();
// Push latest state up to UI
// TODO: if mode change was racing with us, don't overwrite it
@@ -407,9 +409,7 @@ public class DirectoryFragment extends Fragment {
@Override
public void onLoaderReset(Loader<DirectoryResult> loader) {
- // TODO: make the adapter listen to the model.
mModel.update(null);
- mAdapter.update();
}
};
@@ -827,9 +827,9 @@ public class DirectoryFragment extends Fragment {
if (event == Snackbar.Callback.DISMISS_EVENT_ACTION) {
mModel.undoDeletion();
} else {
- mModel.finalizeDeletion();
+ // TODO: Use a listener rather than pushing the view.
+ mModel.finalizeDeletion(DirectoryFragment.this.getView());
}
- ;
}
})
.show();
@@ -953,7 +953,8 @@ public class DirectoryFragment extends Fragment {
}
}
- private final class DocumentsAdapter extends RecyclerView.Adapter<DocumentHolder> {
+ private final class DocumentsAdapter extends RecyclerView.Adapter<DocumentHolder>
+ implements Model.UpdateListener {
private final Context mContext;
private final LayoutInflater mInflater;
@@ -965,19 +966,19 @@ public class DirectoryFragment extends Fragment {
mInflater = LayoutInflater.from(context);
}
- public void update() {
+ public void onModelUpdate(Model model) {
mFooters.clear();
- if (mModel.info != null) {
- mFooters.add(new MessageFooter(2, R.drawable.ic_dialog_info, mModel.info));
+ if (model.info != null) {
+ mFooters.add(new MessageFooter(2, R.drawable.ic_dialog_info, model.info));
}
- if (mModel.error != null) {
- mFooters.add(new MessageFooter(3, R.drawable.ic_dialog_alert, mModel.error));
+ if (model.error != null) {
+ mFooters.add(new MessageFooter(3, R.drawable.ic_dialog_alert, model.error));
}
- if (mModel.isLoading()) {
+ if (model.isLoading()) {
mFooters.add(new LoadingFooter());
}
- if (mModel.isEmpty()) {
+ if (model.isEmpty()) {
mEmptyView.setVisibility(View.VISIBLE);
} else {
mEmptyView.setVisibility(View.GONE);
@@ -986,6 +987,12 @@ public class DirectoryFragment extends Fragment {
notifyDataSetChanged();
}
+ public void onModelUpdateFailed(Exception e) {
+ String error = getString(R.string.query_error);
+ mFooters.add(new MessageFooter(3, R.drawable.ic_dialog_alert, error));
+ notifyDataSetChanged();
+ }
+
@Override
public DocumentHolder onCreateViewHolder(ViewGroup parent, int viewType) {
final State state = getDisplayState(DirectoryFragment.this);
@@ -1736,14 +1743,22 @@ public class DirectoryFragment extends Fragment {
/**
* The data model for the current loaded directory.
*/
- private final class Model implements DocumentContext {
+ @VisibleForTesting
+ public static final class Model implements DocumentContext {
private MultiSelectManager mSelectionManager;
+ private Context mContext;
private int mCursorCount;
private boolean mIsLoading;
+ private SparseBooleanArray mMarkedForDeletion = new SparseBooleanArray();
+ private UpdateListener mUpdateListener;
@Nullable private Cursor mCursor;
@Nullable private String info;
@Nullable private String error;
- private SparseBooleanArray mMarkedForDeletion = new SparseBooleanArray();
+
+ Model(Context context, MultiSelectManager selectionManager) {
+ mContext = context;
+ mSelectionManager = selectionManager;
+ }
/**
* Sets the selection manager used by the model.
@@ -1794,12 +1809,13 @@ public class DirectoryFragment extends Fragment {
info = null;
error = null;
mIsLoading = false;
+ if (mUpdateListener != null) mUpdateListener.onModelUpdate(this);
return;
}
if (result.exception != null) {
Log.e(TAG, "Error while loading directory contents", result.exception);
- error = getString(R.string.query_error);
+ if (mUpdateListener != null) mUpdateListener.onModelUpdateFailed(result.exception);
return;
}
@@ -1812,13 +1828,15 @@ public class DirectoryFragment extends Fragment {
error = extras.getString(DocumentsContract.EXTRA_ERROR);
mIsLoading = extras.getBoolean(DocumentsContract.EXTRA_LOADING, false);
}
+
+ if (mUpdateListener != null) mUpdateListener.onModelUpdate(this);
}
- private int getItemCount() {
+ int getItemCount() {
return mCursorCount - mMarkedForDeletion.size();
}
- private Cursor getItem(int position) {
+ Cursor getItem(int position) {
// Items marked for deletion are masked out of the UI. To do this, for every marked
// item whose position is less than the requested item position, advance the requested
// position by 1.
@@ -1859,7 +1877,7 @@ public class DirectoryFragment extends Fragment {
return getDocuments(sel);
}
- private List<DocumentInfo> getDocuments(Selection items) {
+ List<DocumentInfo> getDocuments(Selection items) {
final int size = (items != null) ? items.size() : 0;
final List<DocumentInfo> docs = new ArrayList<>(size);
@@ -1880,7 +1898,7 @@ public class DirectoryFragment extends Fragment {
return mCursor;
}
- private List<DocumentInfo> getDocumentsMarkedForDeletion() {
+ List<DocumentInfo> getDocumentsMarkedForDeletion() {
final int size = mMarkedForDeletion.size();
List<DocumentInfo> docs = new ArrayList<>(size);
@@ -1901,7 +1919,7 @@ public class DirectoryFragment extends Fragment {
*
* @param selected A selection representing the files to delete.
*/
- public void markForDeletion(Selection selected) {
+ void markForDeletion(Selection selected) {
// Only one deletion operation at a time.
checkState(mMarkedForDeletion.size() == 0);
// There should never be more to delete than what exists.
@@ -1912,7 +1930,7 @@ public class DirectoryFragment extends Fragment {
int position = selected.get(i);
if (DEBUG) Log.d(TAG, "Marked position " + position + " for deletion");
mMarkedForDeletion.append(position, true);
- mAdapter.notifyItemRemoved(position);
+ if (mUpdateListener != null) mUpdateListener.notifyItemRemoved(position);
}
}
@@ -1920,14 +1938,14 @@ public class DirectoryFragment extends Fragment {
* Cancels an ongoing deletion operation. All files currently marked for deletion will be
* unmarked, and restored in the UI. See {@link #markForDeletion(Selection)}.
*/
- public void undoDeletion() {
+ void undoDeletion() {
// Iterate over deleted items, temporarily marking them false in the deletion list, and
// re-adding them to the UI.
final int size = mMarkedForDeletion.size();
for (int i = 0; i < size; ++i) {
final int position = mMarkedForDeletion.keyAt(i);
mMarkedForDeletion.put(position, false);
- mAdapter.notifyItemInserted(position);
+ if (mUpdateListener != null) mUpdateListener.notifyItemInserted(position);
}
// Then, clear the deletion list.
@@ -1937,11 +1955,26 @@ public class DirectoryFragment extends Fragment {
/**
* 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.
*/
- public void finalizeDeletion() {
- final Context context = getActivity();
- final ContentResolver resolver = context.getContentResolver();
- new DeleteFilesTask(resolver).execute();
+ void finalizeDeletion(final View view) {
+ final ContentResolver resolver = mContext.getContentResolver();
+ DeleteFilesTask task = new DeleteFilesTask(
+ resolver,
+ new Runnable() {
+ @Override
+ public void run() {
+ Snackbar.make(
+ view,
+ R.string.toast_failed_delete,
+ Snackbar.LENGTH_LONG)
+ .show();
+
+ }
+ });
+ task.execute();
}
/**
@@ -1950,9 +1983,16 @@ public class DirectoryFragment extends Fragment {
*/
private class DeleteFilesTask extends AsyncTask<Void, Void, List<DocumentInfo>> {
private ContentResolver mResolver;
-
- public DeleteFilesTask(ContentResolver resolver) {
+ private Runnable mErrorCallback;
+
+ /**
+ * @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.
+ */
+ public DeleteFilesTask(ContentResolver resolver, Runnable errorCallback) {
mResolver = resolver;
+ mErrorCallback = errorCallback;
}
@Override
@@ -1985,10 +2025,8 @@ public class DirectoryFragment extends Fragment {
}
if (hadTrouble) {
- // TODO show which files failed?
- Snackbar.make(DirectoryFragment.this.getView(),
- R.string.toast_failed_delete,
- Snackbar.LENGTH_LONG).show();
+ // TODO show which files failed? b/23720103
+ mErrorCallback.run();
if (DEBUG) Log.d(TAG, "Deletion task completed. Some deletions failed.");
} else {
if (DEBUG) Log.d(TAG, "Deletion task completed successfully.");
@@ -1996,5 +2034,32 @@ public class DirectoryFragment extends Fragment {
mMarkedForDeletion.clear();
}
}
+
+ void addUpdateListener(UpdateListener listener) {
+ checkState(mUpdateListener == null);
+ mUpdateListener = listener;
+ }
+
+ interface UpdateListener {
+ /**
+ * Called when a successful update has occurred.
+ */
+ void onModelUpdate(Model model);
+
+ /**
+ * Called when an update has been attempted but failed.
+ */
+ void onModelUpdateFailed(Exception e);
+
+ /**
+ * Called when an item has been removed from the model.
+ */
+ void notifyItemRemoved(int position);
+
+ /**
+ * Called when an item has been added to the model.
+ */
+ void notifyItemInserted(int position);
+ }
}
}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/DirectoryFragmentModelTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/DirectoryFragmentModelTest.java
new file mode 100644
index 000000000000..4331b03291e8
--- /dev/null
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/DirectoryFragmentModelTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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;
+
+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.test.AndroidTestCase;
+import android.test.MoreAsserts;
+import android.test.mock.MockContentResolver;
+
+import com.android.documentsui.DirectoryFragment.Model;
+import com.android.documentsui.MultiSelectManager.Selection;
+import com.android.documentsui.model.DocumentInfo;
+
+import java.util.List;
+
+
+public class DirectoryFragmentModelTest extends AndroidTestCase {
+
+ private static final int ITEM_COUNT = 5;
+ private static final String[] COLUMNS = new String[]{
+ Document.COLUMN_DOCUMENT_ID
+ };
+ private static Cursor cursor;
+
+ private Context mContext;
+ private Model model;
+
+ public void setUp() {
+ setupTestContext();
+
+ MatrixCursor c = new MatrixCursor(COLUMNS);
+ for (int i = 0; i < ITEM_COUNT; ++i) {
+ MatrixCursor.RowBuilder row = c.newRow();
+ row.add(COLUMNS[0], i);
+ }
+ cursor = c;
+
+ DirectoryResult r = new DirectoryResult();
+ r.cursor = cursor;
+
+ model = new Model(mContext, null);
+ 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());
+
+ // Finalize the deletion
+ model.finalizeDeletion(null);
+ 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 c = model.getItem(i);
+ 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... items) {
+ Selection sel = new Selection();
+ for (int item: items) {
+ sel.add(item);
+ }
+ model.markForDeletion(sel);
+ }
+
+ private List<DocumentInfo> getDocumentInfo(int... items) {
+ Selection sel = new Selection();
+ for (int item: items) {
+ sel.add(item);
+ }
+ return model.getDocuments(sel);
+ }
+}