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

Commit d72a1dae authored by Ben Kwa's avatar Ben Kwa
Browse files

Transition selection to use Model IDs.

This CL transitions the MultiSelectManager to (mostly) use Model IDs.

- Add the ability to retrieve all model IDs for the current directory,
  from the model.

- Add a map in the DocumentsAdapter that maps from adapter position to
  model ID.

- Make the adapter listen for model updates, and update its internal map
  of positions to model IDs appropriately.

- Use the aforementioned map when binding ViewHolders.

- Get unit tests to compile; get as many tests passing as possible at
  this point.  Tests related to deleting things won't work right now.

Still to do:

- Add code to the adapter to sort and group items.  After this is done,
  SortingCursorWrapper will no longer be needed.

- Add code to the adapter to deal with item addition/removal.  After
  this is done, the pending-deletion code in the model can be removed.

- Rationalize position-based vs model-based selection.  Some code in the
  MSM (in particular, code dealing with range selection) is still
  position-based.  It's becoming clear that it doesn't make sense for
  range selection to be ID-based, since "range" is a concept that only
  makes sense in the context of items that are positioned in the UI.
  Will need to iterate more on the position-based code to make sure it
  makes sense.

BUG=26024369

Change-Id: I767cde2d888c101aaf83b59155b14634a236164b
parent 0497da8d
Loading
Loading
Loading
Loading
+50 −24
Original line number Diff line number Diff line
@@ -355,6 +355,7 @@ public class DirectoryFragment extends Fragment {
        mSelectionManager.addCallback(new SelectionModeListener());

        mModel = new Model(context, mAdapter);
        mModel.addUpdateListener(mAdapter);
        mModel.addUpdateListener(mModelUpdateListener);

        mType = getArguments().getInt(EXTRA_TYPE);
@@ -638,9 +639,9 @@ public class DirectoryFragment extends Fragment {
        private Menu mMenu;

        @Override
        public boolean onBeforeItemStateChange(int position, boolean selected) {
        public boolean onBeforeItemStateChange(String modelId, boolean selected) {
            if (selected) {
                final Cursor cursor = mModel.getItem(position);
                final Cursor cursor = mModel.getItem(modelId);
                checkNotNull(cursor, "Cursor cannot be null.");
                final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
                final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
@@ -650,8 +651,8 @@ public class DirectoryFragment extends Fragment {
        }

        @Override
        public void onItemStateChanged(int position, boolean selected) {
            final Cursor cursor = mModel.getItem(position);
        public void onItemStateChanged(String modelId, boolean selected) {
            final Cursor cursor = mModel.getItem(modelId);
            checkNotNull(cursor, "Cursor cannot be null.");

            final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
@@ -913,7 +914,7 @@ public class DirectoryFragment extends Fragment {
    // Provide a reference to the views for each data item
    // Complex data items may need more than one view per item, and
    // you provide access to all the views for a data item in a view holder
    private final class DocumentHolder
    final class DocumentHolder
            extends RecyclerView.ViewHolder
            implements View.OnKeyListener
    {
@@ -986,14 +987,23 @@ public class DirectoryFragment extends Fragment {
        mRecView.setVisibility(View.VISIBLE);
    }

    private final class DocumentsAdapter extends RecyclerView.Adapter<DocumentHolder> {
    final class DocumentsAdapter
        extends RecyclerView.Adapter<DocumentHolder>
        implements Model.UpdateListener {

        static private final String TAG = "DocumentsAdapter";
        private final Context mContext;
        private final LayoutInflater mInflater;
        /**
         * Map of model IDs to adapter positions. This is the data structure that determines what
         * shows up in the UI, and where. Note that item positions can change if items are
         * removed/added, so this list should only be used in onBindViewHolder. See
         * {@link RecyclerView.Adapter.onCreateViewHolder} for details.
         */
        // TODO(stable-id): need to keep this up-to-date when items are added/removed
        private List<String> mModelIds = new ArrayList<>();

        public DocumentsAdapter(Context context) {
            mContext = context;
            mInflater = LayoutInflater.from(context);
        }

        @Override
@@ -1029,7 +1039,7 @@ public class DirectoryFragment extends Fragment {
            final View itemView = holder.itemView;

            if (payload.contains(MultiSelectManager.SELECTION_CHANGED_MARKER)) {
                final boolean selected = isSelected(position);
                final boolean selected = isSelected(mModelIds.get(position));
                itemView.setActivated(selected);
                return;
            } else {
@@ -1046,7 +1056,8 @@ public class DirectoryFragment extends Fragment {
            final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache(
                    context, mThumbSize);

            final Cursor cursor = mModel.getItem(position);
            final String modelId = mModelIds.get(position);
            final Cursor cursor = mModel.getItem(modelId);
            checkNotNull(cursor, "Cursor cannot be null.");

            final String docAuthority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY);
@@ -1060,11 +1071,10 @@ public class DirectoryFragment extends Fragment {
            final String docSummary = getCursorString(cursor, Document.COLUMN_SUMMARY);
            final long docSize = getCursorLong(cursor, Document.COLUMN_SIZE);

            // TODO(stable-id): factor the model ID construction code.
            holder.modelId = docAuthority + "|" + docId;
            holder.modelId = Model.createId(cursor);
            final View itemView = holder.itemView;

            holder.setSelected(isSelected(position));
            holder.setSelected(isSelected(modelId));

            final ImageView iconMime = (ImageView) itemView.findViewById(R.id.icon_mime);
            final ImageView iconThumb = (ImageView) itemView.findViewById(R.id.icon_thumb);
@@ -1205,7 +1215,28 @@ public class DirectoryFragment extends Fragment {

        @Override
        public int getItemCount() {
            return mModel.getItemCount();
            if (DEBUG) Log.d(TAG, "getItemCount called: " + mModelIds.size());
            return mModelIds.size();
        }

        @Override
        public void onModelUpdate(Model model) {
            // TODO(stable-id): Sort model IDs, categorize by dir/file, etc
            if (DEBUG) Log.d(TAG, "onModelUpdate called");
            mModelIds = Lists.newArrayList(model.getIds());
        }

        @Override
        public void onModelUpdateFailed(Exception e) {
            if (DEBUG) Log.d(TAG, "onModelUpdateFailed called ");
            mModelIds.clear();
        }

        /**
         * @return The model ID of the item at the given adapter position.
         */
        public String getModelId(int adapterPosition) {
            return mModelIds.get(adapterPosition);
        }

    }
@@ -1399,7 +1430,7 @@ public class DirectoryFragment extends Fragment {
    }

    public void selectAllFiles() {
        boolean changed = mSelectionManager.setItemsSelected(0, mModel.getItemCount(), true);
        boolean changed = mSelectionManager.setItemsSelected(mModel.getIds(), true);
        if (changed) {
            updateDisplayState();
        }
@@ -1670,28 +1701,23 @@ public class DirectoryFragment extends Fragment {
        abstract void onDocumentsReady(List<DocumentInfo> docs);
    }

    boolean isSelected(int position) {
        return mSelectionManager.getSelection().contains(position);
    }

    boolean isSelected(String modelId) {
        // TODO(stable-id): implement this
        return false;
        return mSelectionManager.getSelection().contains(modelId);
    }

    private class ItemClickListener implements ClickListener {
        @Override
        public void onClick(DocumentHolder doc) {
            final int position = doc.getAdapterPosition();
            if (mSelectionManager.hasSelection()) {
                mSelectionManager.toggleSelection(position);
                mSelectionManager.toggleSelection(doc.modelId);
                mSelectionManager.setSelectionRangeBegin(doc.getAdapterPosition());
            } else {
                handleViewItem(doc.modelId);
            }
        }
    }

    private class ModelUpdateListener extends Model.UpdateListener {
    private class ModelUpdateListener implements Model.UpdateListener {
        @Override
        public void onModelUpdate(Model model) {
            if (model.info != null || model.error != null) {
+64 −65
Original line number Diff line number Diff line
@@ -34,7 +34,7 @@ import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.util.SparseBooleanArray;
import android.util.SparseArray;

import com.android.documentsui.BaseActivity.DocumentContext;
import com.android.documentsui.DirectoryResult;
@@ -45,9 +45,10 @@ import com.android.documentsui.model.DocumentInfo;
import com.android.internal.annotations.GuardedBy;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * The data model for the current loaded directory.
@@ -62,8 +63,8 @@ public class Model implements DocumentContext {
    @GuardedBy("mPendingDelete")
    private Boolean mPendingDelete = false;
    @GuardedBy("mPendingDelete")
    private SparseBooleanArray mMarkedForDeletion = new SparseBooleanArray();
    private Model.UpdateListener mUpdateListener;
    private Set<String> mMarkedForDeletion = new HashSet<>();
    private List<UpdateListener> mUpdateListeners = new ArrayList<>();
    @Nullable private Cursor mCursor;
    @Nullable String info;
    @Nullable String error;
@@ -74,6 +75,37 @@ public class Model implements DocumentContext {
        mViewAdapter = viewAdapter;
    }

    /**
     * Generates a Model ID for a cursor entry that refers to a document. The Model ID is a
     * unique string that can be used to identify the document referred to by the cursor.
     *
     * @param c A cursor that refers to a document.
     */
    public static String createId(Cursor c) {
        return getCursorString(c, RootCursorWrapper.COLUMN_AUTHORITY) +
                "|" + getCursorString(c, Document.COLUMN_DOCUMENT_ID);
    }

    /**
     * @return Model IDs for all known items in the model. Note that this will include items
     *         pending deletion.
     */
    public Set<String> getIds() {
        return mPositions.keySet();
    }

    private void notifyUpdateListeners() {
        for (UpdateListener listener: mUpdateListeners) {
            listener.onModelUpdate(this);
        }
    }

    private void notifyUpdateListeners(Exception e) {
        for (UpdateListener listener: mUpdateListeners) {
            listener.onModelUpdateFailed(e);
        }
    }

    void update(DirectoryResult result) {
        if (DEBUG) Log.i(TAG, "Updating model with new result set.");

@@ -83,13 +115,13 @@ public class Model implements DocumentContext {
            info = null;
            error = null;
            mIsLoading = false;
            mUpdateListener.onModelUpdate(this);
            notifyUpdateListeners();
            return;
        }

        if (result.exception != null) {
            Log.e(TAG, "Error while loading directory contents", result.exception);
            mUpdateListener.onModelUpdateFailed(result.exception);
            notifyUpdateListeners(result.exception);
            return;
        }

@@ -105,9 +137,10 @@ public class Model implements DocumentContext {
            mIsLoading = extras.getBoolean(DocumentsContract.EXTRA_LOADING, false);
        }

        mUpdateListener.onModelUpdate(this);
        notifyUpdateListeners();
    }

    @VisibleForTesting
    int getItemCount() {
        synchronized(mPendingDelete) {
            return mCursorCount - mMarkedForDeletion.size();
@@ -122,10 +155,7 @@ public class Model implements DocumentContext {
        mCursor.moveToPosition(-1);
        for (int pos = 0; pos < mCursorCount; ++pos) {
            mCursor.moveToNext();
            // TODO(stable-id): factor the model ID construction code.
            String modelId = getCursorString(mCursor, RootCursorWrapper.COLUMN_AUTHORITY) +
                    "|" + getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID);
            mPositions.put(modelId, pos);
            mPositions.put(Model.createId(mCursor), pos);
        }
    }

@@ -138,36 +168,6 @@ public class Model implements DocumentContext {
        return null;
    }

    Cursor getItem(int position) {
        synchronized(mPendingDelete) {
            // 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.
            final int originalPos = position;
            final int size = mMarkedForDeletion.size();
            for (int i = 0; i < size; ++i) {
                // It'd be more concise, but less efficient, to iterate over positions while calling
                // mMarkedForDeletion.get.  Instead, iterate over deleted entries.
                if (mMarkedForDeletion.keyAt(i) <= position && mMarkedForDeletion.valueAt(i)) {
                    ++position;
                }
            }

            if (DEBUG && position != originalPos) {
                Log.d(TAG, "Item position adjusted for deletion.  Original: " + originalPos
                        + "  Adjusted: " + position);
            }

            if (position >= mCursorCount) {
                throw new IndexOutOfBoundsException("Attempt to retrieve " + position + " of " +
                        mCursorCount + " items");
            }

            mCursor.moveToPosition(position);
            return mCursor;
        }
    }

    boolean isEmpty() {
        return mCursorCount == 0;
    }
@@ -180,8 +180,8 @@ public class Model implements DocumentContext {
        final int size = (items != null) ? items.size() : 0;

        final List<DocumentInfo> docs =  new ArrayList<>(size);
        for (int i = 0; i < size; i++) {
            final Cursor cursor = getItem(items.get(i));
        for (String modelId: items.getAll()) {
            final Cursor cursor = getItem(modelId);
            checkNotNull(cursor, "Cursor cannot be null.");
            final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
            docs.add(doc);
@@ -198,13 +198,14 @@ public class Model implements DocumentContext {
    }

    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 (int i = 0; i < size; ++i) {
                final int position = mMarkedForDeletion.keyAt(i);
                checkState(position < mCursorCount);
            for (String id: mMarkedForDeletion) {
                Integer position = mPositions.get(id);
                checkState(position != null);
                mCursor.moveToPosition(position);
                final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(mCursor);
                docs.add(doc);
@@ -228,15 +229,14 @@ public class Model implements DocumentContext {
            // There should never be more to delete than what exists.
            checkState(mCursorCount >= selected.size());

            int[] positions = selected.getAll();
            Arrays.sort(positions);

            // Walk backwards through the set, since we're removing positions.
            // Otherwise, positions would change after the first modification.
            for (int p = positions.length - 1; p >= 0; p--) {
                mMarkedForDeletion.append(positions[p], true);
                mViewAdapter.notifyItemRemoved(positions[p]);
                if (DEBUG) Log.d(TAG, "Scheduled " + positions[p] + " for delete.");
            // 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.");
            }
        }
    }
@@ -249,11 +249,11 @@ public class Model implements DocumentContext {
        synchronized (mPendingDelete) {
            // 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);
                mViewAdapter.notifyItemInserted(position);
            for (String id: mMarkedForDeletion) {
                Integer pos= mPositions.get(id);
                checkNotNull(pos);
                mMarkedForDeletion.remove(id);
                mViewAdapter.notifyItemInserted(pos);
            }
            resetDeleteData();
        }
@@ -359,19 +359,18 @@ public class Model implements DocumentContext {
    }

    void addUpdateListener(UpdateListener listener) {
        checkState(mUpdateListener == null);
        mUpdateListener = listener;
        mUpdateListeners.add(listener);
    }

    static class UpdateListener {
    static interface UpdateListener {
        /**
         * Called when a successful update has occurred.
         */
        void onModelUpdate(Model model) {}
        void onModelUpdate(Model model);

        /**
         * Called when an update has been attempted but failed.
         */
        void onModelUpdateFailed(Exception e) {}
        void onModelUpdateFailed(Exception e);
    }
}
+244 −272

File changed.

Preview size limit exceeded, changes collapsed.

+22 −10
Original line number Diff line number Diff line
@@ -29,16 +29,19 @@ import android.test.suitebuilder.annotation.SmallTest;
import android.view.ViewGroup;

import com.android.documentsui.DirectoryResult;
import com.android.documentsui.dirlist.MultiSelectManager.Selection;
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 {

    private static final int ITEM_COUNT = 5;
    // 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;
@@ -49,10 +52,17 @@ public class DirectoryFragmentModelTest extends AndroidTestCase {
    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; ++i) {
            MatrixCursor.RowBuilder row = c.newRow();
            row.add(COLUMNS[0], i);
        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;

@@ -101,7 +111,8 @@ public class DirectoryFragmentModelTest extends AndroidTestCase {
    // Tests the base case for Model.getItem.
    public void testGetItem() {
        for (int i = 0; i < ITEM_COUNT; ++i) {
            Cursor c = model.getItem(i);
            cursor.moveToPosition(i);
            Cursor c = model.getItem(Model.createId(cursor));
            assertEquals(i, c.getPosition());
        }
    }
@@ -139,14 +150,15 @@ public class DirectoryFragmentModelTest extends AndroidTestCase {
    }

    private void delete(int... positions) {
        model.markForDeletion(new Selection(positions));
//        model.markForDeletion(new Selection(positions));
    }

    private List<DocumentInfo> getDocumentInfo(int... positions) {
        return model.getDocuments(new Selection(positions));
//        return model.getDocuments(new Selection(positions));
        return new ArrayList<>();
    }

    private static class DummyListener extends Model.UpdateListener {
    private static class DummyListener implements Model.UpdateListener {
        public void onModelUpdate(Model model) {}
        public void onModelUpdateFailed(Exception e) {}
    }
+47 −74
Original line number Diff line number Diff line
@@ -20,13 +20,10 @@ import android.support.v7.widget.RecyclerView;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
import android.util.SparseBooleanArray;
import android.view.View;
import android.view.ViewGroup;

import com.android.documentsui.TestInputEvent;
import com.android.documentsui.dirlist.MultiSelectManager.Selection;

import org.mockito.Mockito;
import com.google.common.collect.Lists;

import java.util.ArrayList;
import java.util.HashSet;
@@ -39,30 +36,25 @@ public class MultiSelectManagerTest extends AndroidTestCase {
    private static final List<String> items;
    static {
        items = new ArrayList<String>();
        items.add("aaa");
        items.add("bbb");
        items.add("ccc");
        items.add("111");
        items.add("222");
        items.add("333");
        for (int i = 0; i < 100; ++i) {
            items.add(Integer.toString(i));
        }
    }

    private MultiSelectManager mManager;
    private TestAdapter mAdapter;
    private TestCallback mCallback;
    private TestSelectionEnvironment mEnv;

    public void setUp() throws Exception {
        mAdapter = new TestAdapter(items);
        mCallback = new TestCallback();
        mEnv = new TestSelectionEnvironment();
        mManager = new MultiSelectManager(mAdapter, mEnv, MultiSelectManager.MODE_MULTIPLE);
        mEnv = new TestSelectionEnvironment(items);
        mManager = new MultiSelectManager(mEnv, MultiSelectManager.MODE_MULTIPLE);
        mManager.addCallback(mCallback);
    }

    public void testMouseClick_StartsSelectionMode() {
        click(7);
        assertSelection(7);
        assertSelection(items.get(7));
    }

    public void testMouseClick_NotifiesSelectionChanged() {
@@ -84,21 +76,21 @@ public class MultiSelectManagerTest extends AndroidTestCase {
    }

    public void testSetSelectionFocusBegin() {
        mManager.setItemSelected(7, true);
        mManager.setSelectionFocusBegin(7);
        mManager.setItemsSelected(Lists.newArrayList(items.get(7)), true);
        mManager.setSelectionRangeBegin(7);
        shiftClick(11);
        assertRangeSelection(7, 11);
    }

    public void testLongPress_StartsSelectionMode() {
        longPress(7);
        assertSelection(7);
        assertSelection(items.get(7));
    }

    public void testLongPress_SecondPressExtendsSelection() {
        longPress(7);
        longPress(99);
        assertSelection(7, 99);
        assertSelection(items.get(7), items.get(99));
    }

    public void testSingleTapUp_UnselectsSelectedItem() {
@@ -118,8 +110,7 @@ public class MultiSelectManagerTest extends AndroidTestCase {
        longPress(99);
        tap(7);
        tap(13);
        tap(129899);
        assertSelection(7, 99, 13, 129899);
        assertSelection(items.get(7), items.get(99), items.get(13));
    }

    public void testSingleTapUp_ShiftCreatesRangeSelection() {
@@ -173,27 +164,27 @@ public class MultiSelectManagerTest extends AndroidTestCase {
    }

    public void testSingleSelectMode() {
        mManager = new MultiSelectManager(mAdapter, mEnv, MultiSelectManager.MODE_SINGLE);
        mManager = new MultiSelectManager(mEnv, MultiSelectManager.MODE_SINGLE);
        mManager.addCallback(mCallback);
        longPress(20);
        tap(13);
        assertSelection(13);
        assertSelection(items.get(13));
    }

    public void testSingleSelectMode_ShiftTap() {
        mManager = new MultiSelectManager(mAdapter, mEnv, MultiSelectManager.MODE_SINGLE);
        mManager = new MultiSelectManager(mEnv, MultiSelectManager.MODE_SINGLE);
        mManager.addCallback(mCallback);
        longPress(13);
        shiftTap(20);
        assertSelection(20);
        assertSelection(items.get(20));
    }

    public void testSingleSelectMode_ShiftDoesNotExtendSelection() {
        mManager = new MultiSelectManager(mAdapter, mEnv, MultiSelectManager.MODE_SINGLE);
        mManager = new MultiSelectManager(mEnv, MultiSelectManager.MODE_SINGLE);
        mManager.addCallback(mCallback);
        longPress(20);
        keyToPosition(22, true);
        assertSelection(22);
        assertSelection(items.get(22));
    }

    public void testProvisionalSelection() {
@@ -203,26 +194,37 @@ public class MultiSelectManagerTest extends AndroidTestCase {
        SparseBooleanArray provisional = new SparseBooleanArray();
        provisional.append(1, true);
        provisional.append(2, true);
        s.setProvisionalSelection(provisional);
        assertSelection(1, 2);
        s.setProvisionalSelection(getItemIds(provisional));
        assertSelection(items.get(1), items.get(2));

        provisional.delete(1);
        provisional.append(3, true);
        s.setProvisionalSelection(provisional);
        assertSelection(2, 3);
        s.setProvisionalSelection(getItemIds(provisional));
        assertSelection(items.get(2), items.get(3));

        s.applyProvisionalSelection();
        assertSelection(2, 3);
        assertSelection(items.get(2), items.get(3));

        provisional.clear();
        provisional.append(3, true);
        provisional.append(4, true);
        s.setProvisionalSelection(provisional);
        assertSelection(2, 3, 4);
        s.setProvisionalSelection(getItemIds(provisional));
        assertSelection(items.get(2), items.get(3), items.get(4));

        provisional.delete(3);
        s.setProvisionalSelection(provisional);
        assertSelection(2, 3, 4);
        s.setProvisionalSelection(getItemIds(provisional));
        assertSelection(items.get(2), items.get(3), items.get(4));
    }

    private static Set<String> getItemIds(SparseBooleanArray selection) {
        Set<String> ids = new HashSet<>();

        int count = selection.size();
        for (int i = 0; i < count; ++i) {
            ids.add(items.get(selection.keyAt(i)));
        }

        return ids;
    }

    private void longPress(int position) {
@@ -246,26 +248,26 @@ public class MultiSelectManagerTest extends AndroidTestCase {
    }

    private void keyToPosition(int position, boolean shift) {
        mManager.attemptChangePosition(position, shift);
        mManager.attemptChangeFocus(position, shift);
    }

    private void assertSelected(int... expected) {
    private void assertSelected(String... expected) {
        for (int i = 0; i < expected.length; i++) {
            Selection selection = mManager.getSelection();
            String err = String.format(
                    "Selection %s does not contain %d", selection, expected[i]);
                    "Selection %s does not contain %s", selection, expected[i]);
            assertTrue(err, selection.contains(expected[i]));
        }
    }

    private void assertSelection(int... expected) {
    private void assertSelection(String... expected) {
        assertSelectionSize(expected.length);
        assertSelected(expected);
    }

    private void assertRangeSelected(int begin, int end) {
        for (int i = begin; i <= end; i++) {
            assertSelected(i);
            assertSelected(items.get(i));
        }
    }

@@ -281,15 +283,15 @@ public class MultiSelectManagerTest extends AndroidTestCase {

    private static final class TestCallback implements MultiSelectManager.Callback {

        Set<Integer> ignored = new HashSet<>();
        Set<String> ignored = new HashSet<>();
        private boolean mSelectionChanged = false;

        @Override
        public void onItemStateChanged(int position, boolean selected) {}
        public void onItemStateChanged(String modelId, boolean selected) {}

        @Override
        public boolean onBeforeItemStateChange(int position, boolean selected) {
            return !ignored.contains(position);
        public boolean onBeforeItemStateChange(String modelId, boolean selected) {
            return !ignored.contains(modelId);
        }

        @Override
@@ -301,33 +303,4 @@ public class MultiSelectManagerTest extends AndroidTestCase {
            assertTrue(mSelectionChanged);
        }
    }

    private static final class TestHolder extends RecyclerView.ViewHolder {
        // each data item is just a string in this case
        public TestHolder(View view) {
            super(view);
        }
    }

    private static final class TestAdapter extends RecyclerView.Adapter<TestHolder> {

        private List<String> mItems;

        public TestAdapter(List<String> items) {
            mItems = items;
        }

        @Override
        public TestHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            return new TestHolder(Mockito.mock(ViewGroup.class));
        }

        @Override
        public void onBindViewHolder(TestHolder holder, int position) {}

        @Override
        public int getItemCount() {
            return mItems.size();
        }
    }
}
Loading