Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 18fce3cd authored by Ben Kwa's avatar Ben Kwa
Browse files

Add unit tests for DirectoryFragment.Model.

Refactor DirectoryFragment.Model to be a static class.
Introduce some unit tests.

BUG=23754695

Change-Id: Iaa064292ab26b23ac7247e49c05ba91033d84a18
parent 55a309f8
Loading
Loading
Loading
Loading
+101 −36
Original line number Diff line number Diff line
@@ -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;
            private Runnable mErrorCallback;

            public DeleteFilesTask(ContentResolver resolver) {
            /**
             * @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);
        }
    }
}
+157 −0
Original line number Diff line number Diff line
/*
 * 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);
    }
}