Loading packages/DocumentsUI/src/com/android/documentsui/State.java +7 −1 Original line number Original line Diff line number Diff line Loading @@ -25,6 +25,7 @@ import android.os.Parcelable; import android.util.Log; import android.util.Log; import android.util.SparseArray; import android.util.SparseArray; import com.android.documentsui.dirlist.MultiSelectManager.Selection; import com.android.documentsui.model.DocumentInfo; import com.android.documentsui.model.DocumentInfo; import com.android.documentsui.model.DocumentStack; import com.android.documentsui.model.DocumentStack; import com.android.documentsui.model.DurableUtils; import com.android.documentsui.model.DurableUtils; Loading Loading @@ -99,8 +100,11 @@ public class State implements android.os.Parcelable { /** Instance state for every shown directory */ /** Instance state for every shown directory */ public HashMap<String, SparseArray<Parcelable>> dirState = new HashMap<>(); public HashMap<String, SparseArray<Parcelable>> dirState = new HashMap<>(); /** UI selection */ public Selection selectedDocuments = new Selection(); /** Currently copying file */ /** Currently copying file */ public List<DocumentInfo> selectedDocumentsForCopy = new ArrayList<DocumentInfo>(); public List<DocumentInfo> selectedDocumentsForCopy = new ArrayList<>(); /** Name of the package that started DocsUI */ /** Name of the package that started DocsUI */ public List<String> excludedAuthorities = new ArrayList<>(); public List<String> excludedAuthorities = new ArrayList<>(); Loading Loading @@ -173,6 +177,7 @@ public class State implements android.os.Parcelable { DurableUtils.writeToParcel(out, stack); DurableUtils.writeToParcel(out, stack); out.writeString(currentSearch); out.writeString(currentSearch); out.writeMap(dirState); out.writeMap(dirState); out.writeParcelable(selectedDocuments, 0); out.writeList(selectedDocumentsForCopy); out.writeList(selectedDocumentsForCopy); out.writeList(excludedAuthorities); out.writeList(excludedAuthorities); out.writeInt(openableOnly ? 1 : 0); out.writeInt(openableOnly ? 1 : 0); Loading Loading @@ -203,6 +208,7 @@ public class State implements android.os.Parcelable { DurableUtils.readFromParcel(in, state.stack); DurableUtils.readFromParcel(in, state.stack); state.currentSearch = in.readString(); state.currentSearch = in.readString(); in.readMap(state.dirState, loader); in.readMap(state.dirState, loader); state.selectedDocuments = in.readParcelable(loader); in.readList(state.selectedDocumentsForCopy, loader); in.readList(state.selectedDocumentsForCopy, loader); in.readList(state.excludedAuthorities, loader); in.readList(state.excludedAuthorities, loader); state.openableOnly = in.readInt() != 0; state.openableOnly = in.readInt() != 0; Loading packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java +28 −6 Original line number Original line Diff line number Diff line Loading @@ -211,9 +211,6 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi final View view = mRecView.getChildAt(i); final View view = mRecView.getChildAt(i); cancelThumbnailTask(view); cancelThumbnailTask(view); } } // Clear any outstanding selection mSelectionManager.clearSelection(); } } @Override @Override Loading @@ -225,6 +222,7 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi final RootInfo root = getArguments().getParcelable(EXTRA_ROOT); final RootInfo root = getArguments().getParcelable(EXTRA_ROOT); final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC); final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC); mStateKey = buildStateKey(root, doc); mIconHelper = new IconHelper(context, MODE_GRID); mIconHelper = new IconHelper(context, MODE_GRID); Loading @@ -244,6 +242,13 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi mRecView.addOnItemTouchListener(mGestureDetector); mRecView.addOnItemTouchListener(mGestureDetector); // final here because we'll manually bump the listener iwhen we had an initial selection, // but only after the model is fully loaded. final SelectionModeListener selectionListener = new SelectionModeListener(); final Selection initialSelection = state.selectedDocuments.hasDirectoryKey(mStateKey) ? state.selectedDocuments : null; // TODO: instead of inserting the view into the constructor, extract listener-creation code // TODO: instead of inserting the view into the constructor, extract listener-creation code // and set the listener on the view after the fact. Then the view doesn't need to be passed // and set the listener on the view after the fact. Then the view doesn't need to be passed // into the selection manager. // into the selection manager. Loading @@ -252,15 +257,16 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi mAdapter, mAdapter, state.allowMultiple state.allowMultiple ? MultiSelectManager.MODE_MULTIPLE ? MultiSelectManager.MODE_MULTIPLE : MultiSelectManager.MODE_SINGLE); : MultiSelectManager.MODE_SINGLE, mSelectionManager.addCallback(new SelectionModeListener()); initialSelection); mSelectionManager.addCallback(selectionListener); mModel = new Model(); mModel = new Model(); mModel.addUpdateListener(mAdapter); mModel.addUpdateListener(mAdapter); mModel.addUpdateListener(mModelUpdateListener); mModel.addUpdateListener(mModelUpdateListener); mType = getArguments().getInt(EXTRA_TYPE); mType = getArguments().getInt(EXTRA_TYPE); mStateKey = buildStateKey(root, doc); mTuner = FragmentTuner.pick(getContext(), state); mTuner = FragmentTuner.pick(getContext(), state); mClipper = new DocumentClipper(context); mClipper = new DocumentClipper(context); Loading Loading @@ -320,6 +326,10 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi updateDisplayState(); updateDisplayState(); if (initialSelection != null) { selectionListener.onSelectionChanged(); } // Restore any previous instance state // Restore any previous instance state final SparseArray<Parcelable> container = state.dirState.remove(mStateKey); final SparseArray<Parcelable> container = state.dirState.remove(mStateKey); if (container != null && !getArguments().getBoolean(EXTRA_IGNORE_STATE, false)) { if (container != null && !getArguments().getBoolean(EXTRA_IGNORE_STATE, false)) { Loading @@ -346,6 +356,18 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi getLoaderManager().restartLoader(LOADER_ID, null, mCallbacks); getLoaderManager().restartLoader(LOADER_ID, null, mCallbacks); } } @Override public void onSaveInstanceState(Bundle outState) { State state = getDisplayState(); if (mSelectionManager.hasSelection()) { mSelectionManager.getSelection(state.selectedDocuments); state.selectedDocuments.setDirectoryKey(mStateKey); if (!state.selectedDocuments.isEmpty()) { if (DEBUG) Log.d(TAG, "Persisted selection: " + state.selectedDocuments); } } } @Override @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { public void onActivityResult(int requestCode, int resultCode, Intent data) { // There's only one request code right now. Replace this with a switch statement or // There's only one request code right now. Replace this with a switch statement or Loading packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java +123 −54 Original line number Original line Diff line number Diff line Loading @@ -23,9 +23,12 @@ import static com.android.internal.util.Preconditions.checkArgument; import static com.android.internal.util.Preconditions.checkNotNull; import static com.android.internal.util.Preconditions.checkNotNull; import static com.android.internal.util.Preconditions.checkState; import static com.android.internal.util.Preconditions.checkState; import android.annotation.IntDef; import android.graphics.Point; import android.graphics.Point; import android.graphics.Rect; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.Nullable; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.support.annotation.VisibleForTesting; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.GridLayoutManager; Loading @@ -41,12 +44,13 @@ import com.android.documentsui.Events.InputEvent; import com.android.documentsui.Events.MotionInputEvent; import com.android.documentsui.Events.MotionInputEvent; import com.android.documentsui.R; import com.android.documentsui.R; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.ArrayList; import java.util.Collection; import java.util.Collection; import java.util.Collections; import java.util.Collections; import java.util.HashMap; import java.util.HashMap; import java.util.HashSet; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.List; import java.util.Map; import java.util.Map; import java.util.Set; import java.util.Set; Loading @@ -58,10 +62,13 @@ import java.util.Set; */ */ public final class MultiSelectManager { public final class MultiSelectManager { /** Selection mode for multiple select. **/ @IntDef(flag = true, value = { MODE_MULTIPLE, MODE_SINGLE }) @Retention(RetentionPolicy.SOURCE) public @interface SelectionMode {} public static final int MODE_MULTIPLE = 0; public static final int MODE_MULTIPLE = 0; /** Selection mode for multiple select. **/ public static final int MODE_SINGLE = 1; public static final int MODE_SINGLE = 1; private static final String TAG = "MultiSelectManager"; private static final String TAG = "MultiSelectManager"; Loading @@ -79,14 +86,19 @@ public final class MultiSelectManager { /** /** * @param recyclerView * @param mode Selection single or multiple selection mode. * @param mode Selection mode * @param initialSelection selection state probably preserved in external state. */ */ public MultiSelectManager( public MultiSelectManager( final RecyclerView recyclerView, DocumentsAdapter adapter, int mode) { final RecyclerView recyclerView, this(new RuntimeSelectionEnvironment(recyclerView), adapter, mode); DocumentsAdapter adapter, @SelectionMode int mode, @Nullable Selection initialSelection) { this(new RuntimeSelectionEnvironment(recyclerView), adapter, mode, initialSelection); if (mode == MODE_MULTIPLE) { if (mode == MODE_MULTIPLE) { // TODO: Don't load this on low memory devices. mBandManager = new BandController(); mBandManager = new BandController(); } } Loading Loading @@ -116,10 +128,18 @@ public final class MultiSelectManager { * @hide * @hide */ */ @VisibleForTesting @VisibleForTesting MultiSelectManager(SelectionEnvironment environment, DocumentsAdapter adapter, int mode) { MultiSelectManager( SelectionEnvironment environment, DocumentsAdapter adapter, @SelectionMode int mode, @Nullable Selection initialSelection) { mEnvironment = checkNotNull(environment, "'environment' cannot be null."); mEnvironment = checkNotNull(environment, "'environment' cannot be null."); mAdapter = checkNotNull(adapter, "'adapter' cannot be null."); mAdapter = checkNotNull(adapter, "'adapter' cannot be null."); mSingleSelect = mode == MODE_SINGLE; mSingleSelect = mode == MODE_SINGLE; if (initialSelection != null) { mSelection.copyFrom(initialSelection); } mAdapter.registerAdapterDataObserver( mAdapter.registerAdapterDataObserver( new RecyclerView.AdapterDataObserver() { new RecyclerView.AdapterDataObserver() { Loading Loading @@ -202,6 +222,13 @@ public final class MultiSelectManager { return dest; return dest; } } /** * Updates selection to include items in {@code selection}. */ public void updateSelection(Selection selection) { setItemsSelected(selection.toList(), true); } /** /** * Sets the selected state of the specified items. Note that the callback will NOT * Sets the selected state of the specified items. Note that the callback will NOT * be consulted to see if an item can be selected. * be consulted to see if an item can be selected. Loading Loading @@ -615,7 +642,7 @@ public final class MultiSelectManager { * Object representing the current selection. Provides read only access * Object representing the current selection. Provides read only access * public access, and private write access. * public access, and private write access. */ */ public static final class Selection { public static final class Selection implements Parcelable { // This class tracks selected items by managing two sets: the saved selection, and the total // This class tracks selected items by managing two sets: the saved selection, and the total // selection. Saved selections are those which have been completed by tapping an item or by // selection. Saved selections are those which have been completed by tapping an item or by Loading @@ -628,8 +655,9 @@ public final class MultiSelectManager { // item A is tapped (and selected), then an in-progress band select covers A then uncovers // item A is tapped (and selected), then an in-progress band select covers A then uncovers // A, A should still be selected as it has been saved. To ensure this behavior, the saved // A, A should still be selected as it has been saved. To ensure this behavior, the saved // selection must be tracked separately. // selection must be tracked separately. private Set<String> mSavedSelection = new HashSet<>(); private Set<String> mSelection = new HashSet<>(); private Set<String> mTotalSelection = new HashSet<>(); private Set<String> mProvisionalSelection = new HashSet<>(); private String mDirectoryKey; @VisibleForTesting @VisibleForTesting public Selection(String... ids) { public Selection(String... ids) { Loading @@ -643,53 +671,70 @@ public final class MultiSelectManager { * @return true if the position is currently selected. * @return true if the position is currently selected. */ */ public boolean contains(@Nullable String id) { public boolean contains(@Nullable String id) { return mTotalSelection.contains(id); return mSelection.contains(id) || mProvisionalSelection.contains(id); } } /** /** * Returns an unordered array of selected positions. * Returns an unordered array of selected positions. */ */ public String[] getAll() { public String[] getAll() { return mTotalSelection.toArray(new String[0]); return toList().toArray(new String[0]); } /** * Returns an unordered array of selected positions (including any * provisional selections current in effect). */ private List<String> toList() { ArrayList<String> selection = new ArrayList<String>(mSelection); selection.addAll(mProvisionalSelection); return selection; } } /** /** * @return size of the selection. * @return size of the selection. */ */ public int size() { public int size() { return mTotalSelection.size(); return mSelection.size() + mProvisionalSelection.size(); } } /** /** * @return true if the selection is empty. * @return true if the selection is empty. */ */ public boolean isEmpty() { public boolean isEmpty() { return mTotalSelection.isEmpty(); return mSelection.isEmpty() && mProvisionalSelection.isEmpty(); } } /** /** * Sets the provisional selection, which is a temporary selection that can be saved, * Sets the provisional selection, which is a temporary selection that can be saved, * canceled, or adjusted at a later time. When a new provision selection is applied, the old * canceled, or adjusted at a later time. When a new provision selection is applied, the old * one (if it exists) is abandoned. * one (if it exists) is abandoned. * @return Array with entry for each position added or removed. Entries which were added * @return Map of ids added or removed. Added ids have a value of true, removed are false. * contain a value of true, and entries which were removed contain a value of false. */ */ @VisibleForTesting @VisibleForTesting protected Map<String, Boolean> setProvisionalSelection(Set<String> provisionalSelection) { protected Map<String, Boolean> setProvisionalSelection(Set<String> newSelection) { Map<String, Boolean> delta = new HashMap<>(); Map<String, Boolean> delta = new HashMap<>(); for (String id: mTotalSelection) { for (String id: mProvisionalSelection) { // Mark each item that used to be in the selection but is unsaved and not in the new // provisional selection. if (!newSelection.contains(id) && !mSelection.contains(id)) { delta.put(id, false); } } for (String id: mSelection) { // Mark each item that used to be in the selection but is unsaved and not in the new // Mark each item that used to be in the selection but is unsaved and not in the new // provisional selection. // provisional selection. if (!provisionalSelection.contains(id) && !mSavedSelection.contains(id)) { if (!newSelection.contains(id)) { delta.put(id, false); delta.put(id, false); } } } } for (String id: provisionalSelection) { for (String id: newSelection) { // Mark each item that was not previously in the selection but is in the new // Mark each item that was not previously in the selection but is in the new // provisional selection. // provisional selection. if (!mTotalSelection.contains(id)) { if (!mSelection.contains(id) && !mProvisionalSelection.contains(id)) { delta.put(id, true); delta.put(id, true); } } } } Loading @@ -700,9 +745,9 @@ public final class MultiSelectManager { for (Map.Entry<String, Boolean> entry: delta.entrySet()) { for (Map.Entry<String, Boolean> entry: delta.entrySet()) { String id = entry.getKey(); String id = entry.getKey(); if (entry.getValue()) { if (entry.getValue()) { mTotalSelection.add(id); mProvisionalSelection.add(id); } else { } else { mTotalSelection.remove(id); mProvisionalSelection.remove(id); } } } } Loading @@ -716,7 +761,8 @@ public final class MultiSelectManager { */ */ @VisibleForTesting @VisibleForTesting protected void applyProvisionalSelection() { protected void applyProvisionalSelection() { mSavedSelection = new HashSet<>(mTotalSelection); mSelection.addAll(mProvisionalSelection); mProvisionalSelection.clear(); } } /** /** Loading @@ -725,15 +771,14 @@ public final class MultiSelectManager { */ */ @VisibleForTesting @VisibleForTesting void cancelProvisionalSelection() { void cancelProvisionalSelection() { mTotalSelection = new HashSet<>(mSavedSelection); mProvisionalSelection.clear(); } } /** @hide */ /** @hide */ @VisibleForTesting @VisibleForTesting boolean add(String id) { boolean add(String id) { if (!mTotalSelection.contains(id)) { if (!mSelection.contains(id)) { mTotalSelection.add(id); mSelection.add(id); mSavedSelection.add(id); return true; return true; } } return false; return false; Loading @@ -742,31 +787,29 @@ public final class MultiSelectManager { /** @hide */ /** @hide */ @VisibleForTesting @VisibleForTesting boolean remove(String id) { boolean remove(String id) { if (mTotalSelection.contains(id)) { if (mSelection.contains(id)) { mTotalSelection.remove(id); mSelection.remove(id); mSavedSelection.remove(id); return true; return true; } } return false; return false; } } public void clear() { public void clear() { mSavedSelection.clear(); mSelection.clear(); mTotalSelection.clear(); } } /** /** * Trims this selection to be the intersection of itself with the set of given IDs. * Trims this selection to be the intersection of itself with the set of given IDs. */ */ public void intersect(Collection<String> ids) { public void intersect(Collection<String> ids) { mSavedSelection.retainAll(ids); mSelection.retainAll(ids); mTotalSelection.retainAll(ids); mProvisionalSelection.retainAll(ids); } } @VisibleForTesting @VisibleForTesting void copyFrom(Selection source) { void copyFrom(Selection source) { mSavedSelection = new HashSet<>(source.mSavedSelection); mSelection = new HashSet<>(source.mSelection); mTotalSelection = new HashSet<>(source.mTotalSelection); mProvisionalSelection = new HashSet<>(source.mProvisionalSelection); } } @Override @Override Loading @@ -775,24 +818,19 @@ public final class MultiSelectManager { return "size=0, items=[]"; return "size=0, items=[]"; } } StringBuilder buffer = new StringBuilder(mTotalSelection.size() * 28); StringBuilder buffer = new StringBuilder(size() * 28); buffer.append("{size=") buffer.append("Selection{") .append(mTotalSelection.size()) .append("applied{size=" + mSelection.size()) .append(", ") .append(", entries=" + mSelection) .append("items=["); .append("}, provisional{size=" + mProvisionalSelection.size()) for (Iterator<String> i = mTotalSelection.iterator(); i.hasNext(); ) { .append(", entries=" + mProvisionalSelection) buffer.append(i.next()); .append("}}"); if (i.hasNext()) { buffer.append(", "); } } buffer.append("]}"); return buffer.toString(); return buffer.toString(); } } @Override @Override public int hashCode() { public int hashCode() { return mSavedSelection.hashCode() ^ mTotalSelection.hashCode(); return mSelection.hashCode() ^ mProvisionalSelection.hashCode(); } } @Override @Override Loading @@ -805,8 +843,39 @@ public final class MultiSelectManager { return false; return false; } } return mSavedSelection.equals(((Selection) that).mSavedSelection) && return mSelection.equals(((Selection) that).mSelection) && mTotalSelection.equals(((Selection) that).mTotalSelection); mProvisionalSelection.equals(((Selection) that).mProvisionalSelection); } /** * Sets the state key for this selection, which allows us to match selections * to particular states (of DirectoryFragment). Basically this lets us avoid * loading a persisted selection in the wrong directory. */ public void setDirectoryKey(String key) { mDirectoryKey = key; } /** * Sets the state key for this selection, which allows us to match selections * to particular states (of DirectoryFragment). Basically this lets us avoid * loading a persisted selection in the wrong directory. */ public boolean hasDirectoryKey(String key) { return key.equals(mDirectoryKey); } @Override public int describeContents() { return 0; } public void writeToParcel(Parcel dest, int flags) { checkState(mDirectoryKey != null); dest.writeString(mDirectoryKey); dest.writeList(new ArrayList<>(mSelection)); // We don't include provisional selection since it is // typically coupled to some other runtime state (like a band). } } } } Loading packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java +59 −10 Original line number Original line Diff line number Diff line Loading @@ -50,7 +50,7 @@ public class MultiSelectManagerTest extends AndroidTestCase { mCallback = new TestCallback(); mCallback = new TestCallback(); mEnv = new TestSelectionEnvironment(items); mEnv = new TestSelectionEnvironment(items); mAdapter = new TestDocumentsAdapter(items); mAdapter = new TestDocumentsAdapter(items); mManager = new MultiSelectManager(mEnv, mAdapter, MultiSelectManager.MODE_MULTIPLE); mManager = new MultiSelectManager(mEnv, mAdapter, MultiSelectManager.MODE_MULTIPLE, null); mManager.addCallback(mCallback); mManager.addCallback(mCallback); } } Loading Loading @@ -174,7 +174,7 @@ public class MultiSelectManagerTest extends AndroidTestCase { } } public void testSingleSelectMode() { public void testSingleSelectMode() { mManager = new MultiSelectManager(mEnv, mAdapter, MultiSelectManager.MODE_SINGLE); mManager = new MultiSelectManager(mEnv, mAdapter, MultiSelectManager.MODE_SINGLE, null); mManager.addCallback(mCallback); mManager.addCallback(mCallback); longPress(20); longPress(20); tap(13); tap(13); Loading @@ -182,7 +182,7 @@ public class MultiSelectManagerTest extends AndroidTestCase { } } public void testSingleSelectMode_ShiftTap() { public void testSingleSelectMode_ShiftTap() { mManager = new MultiSelectManager(mEnv, mAdapter, MultiSelectManager.MODE_SINGLE); mManager = new MultiSelectManager(mEnv, mAdapter, MultiSelectManager.MODE_SINGLE, null); mManager.addCallback(mCallback); mManager.addCallback(mCallback); longPress(13); longPress(13); shiftTap(20); shiftTap(20); Loading @@ -198,24 +198,73 @@ public class MultiSelectManagerTest extends AndroidTestCase { provisional.append(2, true); provisional.append(2, true); s.setProvisionalSelection(getItemIds(provisional)); s.setProvisionalSelection(getItemIds(provisional)); assertSelection(items.get(1), items.get(2)); assertSelection(items.get(1), items.get(2)); } public void testProvisionalSelection_Replace() { Selection s = mManager.getSelection(); provisional.delete(1); SparseBooleanArray provisional = new SparseBooleanArray(); provisional.append(1, true); provisional.append(2, true); s.setProvisionalSelection(getItemIds(provisional)); provisional.clear(); provisional.append(3, true); provisional.append(3, true); provisional.append(4, true); s.setProvisionalSelection(getItemIds(provisional)); s.setProvisionalSelection(getItemIds(provisional)); assertSelection(items.get(2), items.get(3)); assertSelection(items.get(3), items.get(4)); } s.applyProvisionalSelection(); public void testProvisionalSelection_IntersectsExistingProvisionalSelection() { assertSelection(items.get(2), items.get(3)); Selection s = mManager.getSelection(); SparseBooleanArray provisional = new SparseBooleanArray(); provisional.append(1, true); provisional.append(2, true); s.setProvisionalSelection(getItemIds(provisional)); provisional.clear(); provisional.clear(); provisional.append(1, true); s.setProvisionalSelection(getItemIds(provisional)); assertSelection(items.get(1)); } public void testProvisionalSelection_Apply() { Selection s = mManager.getSelection(); SparseBooleanArray provisional = new SparseBooleanArray(); provisional.append(1, true); provisional.append(2, true); s.setProvisionalSelection(getItemIds(provisional)); s.applyProvisionalSelection(); assertSelection(items.get(1), items.get(2)); } public void testProvisionalSelection_Cancel() { mManager.toggleSelection(items.get(1)); mManager.toggleSelection(items.get(2)); Selection s = mManager.getSelection(); SparseBooleanArray provisional = new SparseBooleanArray(); provisional.append(3, true); provisional.append(3, true); provisional.append(4, true); provisional.append(4, true); s.setProvisionalSelection(getItemIds(provisional)); s.setProvisionalSelection(getItemIds(provisional)); assertSelection(items.get(2), items.get(3), items.get(4)); s.cancelProvisionalSelection(); // Original selection should remain. assertSelection(items.get(1), items.get(2)); } provisional.delete(3); public void testProvisionalSelection_IntersectsAppliedSelection() { mManager.toggleSelection(items.get(1)); mManager.toggleSelection(items.get(2)); Selection s = mManager.getSelection(); SparseBooleanArray provisional = new SparseBooleanArray(); provisional.append(2, true); provisional.append(3, true); s.setProvisionalSelection(getItemIds(provisional)); s.setProvisionalSelection(getItemIds(provisional)); assertSelection(items.get(2), items.get(3), items.get(4)); assertSelection(items.get(1), items.get(2), items.get(3)); } } private static Set<String> getItemIds(SparseBooleanArray selection) { private static Set<String> getItemIds(SparseBooleanArray selection) { Loading Loading
packages/DocumentsUI/src/com/android/documentsui/State.java +7 −1 Original line number Original line Diff line number Diff line Loading @@ -25,6 +25,7 @@ import android.os.Parcelable; import android.util.Log; import android.util.Log; import android.util.SparseArray; import android.util.SparseArray; import com.android.documentsui.dirlist.MultiSelectManager.Selection; import com.android.documentsui.model.DocumentInfo; import com.android.documentsui.model.DocumentInfo; import com.android.documentsui.model.DocumentStack; import com.android.documentsui.model.DocumentStack; import com.android.documentsui.model.DurableUtils; import com.android.documentsui.model.DurableUtils; Loading Loading @@ -99,8 +100,11 @@ public class State implements android.os.Parcelable { /** Instance state for every shown directory */ /** Instance state for every shown directory */ public HashMap<String, SparseArray<Parcelable>> dirState = new HashMap<>(); public HashMap<String, SparseArray<Parcelable>> dirState = new HashMap<>(); /** UI selection */ public Selection selectedDocuments = new Selection(); /** Currently copying file */ /** Currently copying file */ public List<DocumentInfo> selectedDocumentsForCopy = new ArrayList<DocumentInfo>(); public List<DocumentInfo> selectedDocumentsForCopy = new ArrayList<>(); /** Name of the package that started DocsUI */ /** Name of the package that started DocsUI */ public List<String> excludedAuthorities = new ArrayList<>(); public List<String> excludedAuthorities = new ArrayList<>(); Loading Loading @@ -173,6 +177,7 @@ public class State implements android.os.Parcelable { DurableUtils.writeToParcel(out, stack); DurableUtils.writeToParcel(out, stack); out.writeString(currentSearch); out.writeString(currentSearch); out.writeMap(dirState); out.writeMap(dirState); out.writeParcelable(selectedDocuments, 0); out.writeList(selectedDocumentsForCopy); out.writeList(selectedDocumentsForCopy); out.writeList(excludedAuthorities); out.writeList(excludedAuthorities); out.writeInt(openableOnly ? 1 : 0); out.writeInt(openableOnly ? 1 : 0); Loading Loading @@ -203,6 +208,7 @@ public class State implements android.os.Parcelable { DurableUtils.readFromParcel(in, state.stack); DurableUtils.readFromParcel(in, state.stack); state.currentSearch = in.readString(); state.currentSearch = in.readString(); in.readMap(state.dirState, loader); in.readMap(state.dirState, loader); state.selectedDocuments = in.readParcelable(loader); in.readList(state.selectedDocumentsForCopy, loader); in.readList(state.selectedDocumentsForCopy, loader); in.readList(state.excludedAuthorities, loader); in.readList(state.excludedAuthorities, loader); state.openableOnly = in.readInt() != 0; state.openableOnly = in.readInt() != 0; Loading
packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java +28 −6 Original line number Original line Diff line number Diff line Loading @@ -211,9 +211,6 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi final View view = mRecView.getChildAt(i); final View view = mRecView.getChildAt(i); cancelThumbnailTask(view); cancelThumbnailTask(view); } } // Clear any outstanding selection mSelectionManager.clearSelection(); } } @Override @Override Loading @@ -225,6 +222,7 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi final RootInfo root = getArguments().getParcelable(EXTRA_ROOT); final RootInfo root = getArguments().getParcelable(EXTRA_ROOT); final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC); final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC); mStateKey = buildStateKey(root, doc); mIconHelper = new IconHelper(context, MODE_GRID); mIconHelper = new IconHelper(context, MODE_GRID); Loading @@ -244,6 +242,13 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi mRecView.addOnItemTouchListener(mGestureDetector); mRecView.addOnItemTouchListener(mGestureDetector); // final here because we'll manually bump the listener iwhen we had an initial selection, // but only after the model is fully loaded. final SelectionModeListener selectionListener = new SelectionModeListener(); final Selection initialSelection = state.selectedDocuments.hasDirectoryKey(mStateKey) ? state.selectedDocuments : null; // TODO: instead of inserting the view into the constructor, extract listener-creation code // TODO: instead of inserting the view into the constructor, extract listener-creation code // and set the listener on the view after the fact. Then the view doesn't need to be passed // and set the listener on the view after the fact. Then the view doesn't need to be passed // into the selection manager. // into the selection manager. Loading @@ -252,15 +257,16 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi mAdapter, mAdapter, state.allowMultiple state.allowMultiple ? MultiSelectManager.MODE_MULTIPLE ? MultiSelectManager.MODE_MULTIPLE : MultiSelectManager.MODE_SINGLE); : MultiSelectManager.MODE_SINGLE, mSelectionManager.addCallback(new SelectionModeListener()); initialSelection); mSelectionManager.addCallback(selectionListener); mModel = new Model(); mModel = new Model(); mModel.addUpdateListener(mAdapter); mModel.addUpdateListener(mAdapter); mModel.addUpdateListener(mModelUpdateListener); mModel.addUpdateListener(mModelUpdateListener); mType = getArguments().getInt(EXTRA_TYPE); mType = getArguments().getInt(EXTRA_TYPE); mStateKey = buildStateKey(root, doc); mTuner = FragmentTuner.pick(getContext(), state); mTuner = FragmentTuner.pick(getContext(), state); mClipper = new DocumentClipper(context); mClipper = new DocumentClipper(context); Loading Loading @@ -320,6 +326,10 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi updateDisplayState(); updateDisplayState(); if (initialSelection != null) { selectionListener.onSelectionChanged(); } // Restore any previous instance state // Restore any previous instance state final SparseArray<Parcelable> container = state.dirState.remove(mStateKey); final SparseArray<Parcelable> container = state.dirState.remove(mStateKey); if (container != null && !getArguments().getBoolean(EXTRA_IGNORE_STATE, false)) { if (container != null && !getArguments().getBoolean(EXTRA_IGNORE_STATE, false)) { Loading @@ -346,6 +356,18 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi getLoaderManager().restartLoader(LOADER_ID, null, mCallbacks); getLoaderManager().restartLoader(LOADER_ID, null, mCallbacks); } } @Override public void onSaveInstanceState(Bundle outState) { State state = getDisplayState(); if (mSelectionManager.hasSelection()) { mSelectionManager.getSelection(state.selectedDocuments); state.selectedDocuments.setDirectoryKey(mStateKey); if (!state.selectedDocuments.isEmpty()) { if (DEBUG) Log.d(TAG, "Persisted selection: " + state.selectedDocuments); } } } @Override @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { public void onActivityResult(int requestCode, int resultCode, Intent data) { // There's only one request code right now. Replace this with a switch statement or // There's only one request code right now. Replace this with a switch statement or Loading
packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java +123 −54 Original line number Original line Diff line number Diff line Loading @@ -23,9 +23,12 @@ import static com.android.internal.util.Preconditions.checkArgument; import static com.android.internal.util.Preconditions.checkNotNull; import static com.android.internal.util.Preconditions.checkNotNull; import static com.android.internal.util.Preconditions.checkState; import static com.android.internal.util.Preconditions.checkState; import android.annotation.IntDef; import android.graphics.Point; import android.graphics.Point; import android.graphics.Rect; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.Nullable; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.support.annotation.VisibleForTesting; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.GridLayoutManager; Loading @@ -41,12 +44,13 @@ import com.android.documentsui.Events.InputEvent; import com.android.documentsui.Events.MotionInputEvent; import com.android.documentsui.Events.MotionInputEvent; import com.android.documentsui.R; import com.android.documentsui.R; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.ArrayList; import java.util.Collection; import java.util.Collection; import java.util.Collections; import java.util.Collections; import java.util.HashMap; import java.util.HashMap; import java.util.HashSet; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.List; import java.util.Map; import java.util.Map; import java.util.Set; import java.util.Set; Loading @@ -58,10 +62,13 @@ import java.util.Set; */ */ public final class MultiSelectManager { public final class MultiSelectManager { /** Selection mode for multiple select. **/ @IntDef(flag = true, value = { MODE_MULTIPLE, MODE_SINGLE }) @Retention(RetentionPolicy.SOURCE) public @interface SelectionMode {} public static final int MODE_MULTIPLE = 0; public static final int MODE_MULTIPLE = 0; /** Selection mode for multiple select. **/ public static final int MODE_SINGLE = 1; public static final int MODE_SINGLE = 1; private static final String TAG = "MultiSelectManager"; private static final String TAG = "MultiSelectManager"; Loading @@ -79,14 +86,19 @@ public final class MultiSelectManager { /** /** * @param recyclerView * @param mode Selection single or multiple selection mode. * @param mode Selection mode * @param initialSelection selection state probably preserved in external state. */ */ public MultiSelectManager( public MultiSelectManager( final RecyclerView recyclerView, DocumentsAdapter adapter, int mode) { final RecyclerView recyclerView, this(new RuntimeSelectionEnvironment(recyclerView), adapter, mode); DocumentsAdapter adapter, @SelectionMode int mode, @Nullable Selection initialSelection) { this(new RuntimeSelectionEnvironment(recyclerView), adapter, mode, initialSelection); if (mode == MODE_MULTIPLE) { if (mode == MODE_MULTIPLE) { // TODO: Don't load this on low memory devices. mBandManager = new BandController(); mBandManager = new BandController(); } } Loading Loading @@ -116,10 +128,18 @@ public final class MultiSelectManager { * @hide * @hide */ */ @VisibleForTesting @VisibleForTesting MultiSelectManager(SelectionEnvironment environment, DocumentsAdapter adapter, int mode) { MultiSelectManager( SelectionEnvironment environment, DocumentsAdapter adapter, @SelectionMode int mode, @Nullable Selection initialSelection) { mEnvironment = checkNotNull(environment, "'environment' cannot be null."); mEnvironment = checkNotNull(environment, "'environment' cannot be null."); mAdapter = checkNotNull(adapter, "'adapter' cannot be null."); mAdapter = checkNotNull(adapter, "'adapter' cannot be null."); mSingleSelect = mode == MODE_SINGLE; mSingleSelect = mode == MODE_SINGLE; if (initialSelection != null) { mSelection.copyFrom(initialSelection); } mAdapter.registerAdapterDataObserver( mAdapter.registerAdapterDataObserver( new RecyclerView.AdapterDataObserver() { new RecyclerView.AdapterDataObserver() { Loading Loading @@ -202,6 +222,13 @@ public final class MultiSelectManager { return dest; return dest; } } /** * Updates selection to include items in {@code selection}. */ public void updateSelection(Selection selection) { setItemsSelected(selection.toList(), true); } /** /** * Sets the selected state of the specified items. Note that the callback will NOT * Sets the selected state of the specified items. Note that the callback will NOT * be consulted to see if an item can be selected. * be consulted to see if an item can be selected. Loading Loading @@ -615,7 +642,7 @@ public final class MultiSelectManager { * Object representing the current selection. Provides read only access * Object representing the current selection. Provides read only access * public access, and private write access. * public access, and private write access. */ */ public static final class Selection { public static final class Selection implements Parcelable { // This class tracks selected items by managing two sets: the saved selection, and the total // This class tracks selected items by managing two sets: the saved selection, and the total // selection. Saved selections are those which have been completed by tapping an item or by // selection. Saved selections are those which have been completed by tapping an item or by Loading @@ -628,8 +655,9 @@ public final class MultiSelectManager { // item A is tapped (and selected), then an in-progress band select covers A then uncovers // item A is tapped (and selected), then an in-progress band select covers A then uncovers // A, A should still be selected as it has been saved. To ensure this behavior, the saved // A, A should still be selected as it has been saved. To ensure this behavior, the saved // selection must be tracked separately. // selection must be tracked separately. private Set<String> mSavedSelection = new HashSet<>(); private Set<String> mSelection = new HashSet<>(); private Set<String> mTotalSelection = new HashSet<>(); private Set<String> mProvisionalSelection = new HashSet<>(); private String mDirectoryKey; @VisibleForTesting @VisibleForTesting public Selection(String... ids) { public Selection(String... ids) { Loading @@ -643,53 +671,70 @@ public final class MultiSelectManager { * @return true if the position is currently selected. * @return true if the position is currently selected. */ */ public boolean contains(@Nullable String id) { public boolean contains(@Nullable String id) { return mTotalSelection.contains(id); return mSelection.contains(id) || mProvisionalSelection.contains(id); } } /** /** * Returns an unordered array of selected positions. * Returns an unordered array of selected positions. */ */ public String[] getAll() { public String[] getAll() { return mTotalSelection.toArray(new String[0]); return toList().toArray(new String[0]); } /** * Returns an unordered array of selected positions (including any * provisional selections current in effect). */ private List<String> toList() { ArrayList<String> selection = new ArrayList<String>(mSelection); selection.addAll(mProvisionalSelection); return selection; } } /** /** * @return size of the selection. * @return size of the selection. */ */ public int size() { public int size() { return mTotalSelection.size(); return mSelection.size() + mProvisionalSelection.size(); } } /** /** * @return true if the selection is empty. * @return true if the selection is empty. */ */ public boolean isEmpty() { public boolean isEmpty() { return mTotalSelection.isEmpty(); return mSelection.isEmpty() && mProvisionalSelection.isEmpty(); } } /** /** * Sets the provisional selection, which is a temporary selection that can be saved, * Sets the provisional selection, which is a temporary selection that can be saved, * canceled, or adjusted at a later time. When a new provision selection is applied, the old * canceled, or adjusted at a later time. When a new provision selection is applied, the old * one (if it exists) is abandoned. * one (if it exists) is abandoned. * @return Array with entry for each position added or removed. Entries which were added * @return Map of ids added or removed. Added ids have a value of true, removed are false. * contain a value of true, and entries which were removed contain a value of false. */ */ @VisibleForTesting @VisibleForTesting protected Map<String, Boolean> setProvisionalSelection(Set<String> provisionalSelection) { protected Map<String, Boolean> setProvisionalSelection(Set<String> newSelection) { Map<String, Boolean> delta = new HashMap<>(); Map<String, Boolean> delta = new HashMap<>(); for (String id: mTotalSelection) { for (String id: mProvisionalSelection) { // Mark each item that used to be in the selection but is unsaved and not in the new // provisional selection. if (!newSelection.contains(id) && !mSelection.contains(id)) { delta.put(id, false); } } for (String id: mSelection) { // Mark each item that used to be in the selection but is unsaved and not in the new // Mark each item that used to be in the selection but is unsaved and not in the new // provisional selection. // provisional selection. if (!provisionalSelection.contains(id) && !mSavedSelection.contains(id)) { if (!newSelection.contains(id)) { delta.put(id, false); delta.put(id, false); } } } } for (String id: provisionalSelection) { for (String id: newSelection) { // Mark each item that was not previously in the selection but is in the new // Mark each item that was not previously in the selection but is in the new // provisional selection. // provisional selection. if (!mTotalSelection.contains(id)) { if (!mSelection.contains(id) && !mProvisionalSelection.contains(id)) { delta.put(id, true); delta.put(id, true); } } } } Loading @@ -700,9 +745,9 @@ public final class MultiSelectManager { for (Map.Entry<String, Boolean> entry: delta.entrySet()) { for (Map.Entry<String, Boolean> entry: delta.entrySet()) { String id = entry.getKey(); String id = entry.getKey(); if (entry.getValue()) { if (entry.getValue()) { mTotalSelection.add(id); mProvisionalSelection.add(id); } else { } else { mTotalSelection.remove(id); mProvisionalSelection.remove(id); } } } } Loading @@ -716,7 +761,8 @@ public final class MultiSelectManager { */ */ @VisibleForTesting @VisibleForTesting protected void applyProvisionalSelection() { protected void applyProvisionalSelection() { mSavedSelection = new HashSet<>(mTotalSelection); mSelection.addAll(mProvisionalSelection); mProvisionalSelection.clear(); } } /** /** Loading @@ -725,15 +771,14 @@ public final class MultiSelectManager { */ */ @VisibleForTesting @VisibleForTesting void cancelProvisionalSelection() { void cancelProvisionalSelection() { mTotalSelection = new HashSet<>(mSavedSelection); mProvisionalSelection.clear(); } } /** @hide */ /** @hide */ @VisibleForTesting @VisibleForTesting boolean add(String id) { boolean add(String id) { if (!mTotalSelection.contains(id)) { if (!mSelection.contains(id)) { mTotalSelection.add(id); mSelection.add(id); mSavedSelection.add(id); return true; return true; } } return false; return false; Loading @@ -742,31 +787,29 @@ public final class MultiSelectManager { /** @hide */ /** @hide */ @VisibleForTesting @VisibleForTesting boolean remove(String id) { boolean remove(String id) { if (mTotalSelection.contains(id)) { if (mSelection.contains(id)) { mTotalSelection.remove(id); mSelection.remove(id); mSavedSelection.remove(id); return true; return true; } } return false; return false; } } public void clear() { public void clear() { mSavedSelection.clear(); mSelection.clear(); mTotalSelection.clear(); } } /** /** * Trims this selection to be the intersection of itself with the set of given IDs. * Trims this selection to be the intersection of itself with the set of given IDs. */ */ public void intersect(Collection<String> ids) { public void intersect(Collection<String> ids) { mSavedSelection.retainAll(ids); mSelection.retainAll(ids); mTotalSelection.retainAll(ids); mProvisionalSelection.retainAll(ids); } } @VisibleForTesting @VisibleForTesting void copyFrom(Selection source) { void copyFrom(Selection source) { mSavedSelection = new HashSet<>(source.mSavedSelection); mSelection = new HashSet<>(source.mSelection); mTotalSelection = new HashSet<>(source.mTotalSelection); mProvisionalSelection = new HashSet<>(source.mProvisionalSelection); } } @Override @Override Loading @@ -775,24 +818,19 @@ public final class MultiSelectManager { return "size=0, items=[]"; return "size=0, items=[]"; } } StringBuilder buffer = new StringBuilder(mTotalSelection.size() * 28); StringBuilder buffer = new StringBuilder(size() * 28); buffer.append("{size=") buffer.append("Selection{") .append(mTotalSelection.size()) .append("applied{size=" + mSelection.size()) .append(", ") .append(", entries=" + mSelection) .append("items=["); .append("}, provisional{size=" + mProvisionalSelection.size()) for (Iterator<String> i = mTotalSelection.iterator(); i.hasNext(); ) { .append(", entries=" + mProvisionalSelection) buffer.append(i.next()); .append("}}"); if (i.hasNext()) { buffer.append(", "); } } buffer.append("]}"); return buffer.toString(); return buffer.toString(); } } @Override @Override public int hashCode() { public int hashCode() { return mSavedSelection.hashCode() ^ mTotalSelection.hashCode(); return mSelection.hashCode() ^ mProvisionalSelection.hashCode(); } } @Override @Override Loading @@ -805,8 +843,39 @@ public final class MultiSelectManager { return false; return false; } } return mSavedSelection.equals(((Selection) that).mSavedSelection) && return mSelection.equals(((Selection) that).mSelection) && mTotalSelection.equals(((Selection) that).mTotalSelection); mProvisionalSelection.equals(((Selection) that).mProvisionalSelection); } /** * Sets the state key for this selection, which allows us to match selections * to particular states (of DirectoryFragment). Basically this lets us avoid * loading a persisted selection in the wrong directory. */ public void setDirectoryKey(String key) { mDirectoryKey = key; } /** * Sets the state key for this selection, which allows us to match selections * to particular states (of DirectoryFragment). Basically this lets us avoid * loading a persisted selection in the wrong directory. */ public boolean hasDirectoryKey(String key) { return key.equals(mDirectoryKey); } @Override public int describeContents() { return 0; } public void writeToParcel(Parcel dest, int flags) { checkState(mDirectoryKey != null); dest.writeString(mDirectoryKey); dest.writeList(new ArrayList<>(mSelection)); // We don't include provisional selection since it is // typically coupled to some other runtime state (like a band). } } } } Loading
packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java +59 −10 Original line number Original line Diff line number Diff line Loading @@ -50,7 +50,7 @@ public class MultiSelectManagerTest extends AndroidTestCase { mCallback = new TestCallback(); mCallback = new TestCallback(); mEnv = new TestSelectionEnvironment(items); mEnv = new TestSelectionEnvironment(items); mAdapter = new TestDocumentsAdapter(items); mAdapter = new TestDocumentsAdapter(items); mManager = new MultiSelectManager(mEnv, mAdapter, MultiSelectManager.MODE_MULTIPLE); mManager = new MultiSelectManager(mEnv, mAdapter, MultiSelectManager.MODE_MULTIPLE, null); mManager.addCallback(mCallback); mManager.addCallback(mCallback); } } Loading Loading @@ -174,7 +174,7 @@ public class MultiSelectManagerTest extends AndroidTestCase { } } public void testSingleSelectMode() { public void testSingleSelectMode() { mManager = new MultiSelectManager(mEnv, mAdapter, MultiSelectManager.MODE_SINGLE); mManager = new MultiSelectManager(mEnv, mAdapter, MultiSelectManager.MODE_SINGLE, null); mManager.addCallback(mCallback); mManager.addCallback(mCallback); longPress(20); longPress(20); tap(13); tap(13); Loading @@ -182,7 +182,7 @@ public class MultiSelectManagerTest extends AndroidTestCase { } } public void testSingleSelectMode_ShiftTap() { public void testSingleSelectMode_ShiftTap() { mManager = new MultiSelectManager(mEnv, mAdapter, MultiSelectManager.MODE_SINGLE); mManager = new MultiSelectManager(mEnv, mAdapter, MultiSelectManager.MODE_SINGLE, null); mManager.addCallback(mCallback); mManager.addCallback(mCallback); longPress(13); longPress(13); shiftTap(20); shiftTap(20); Loading @@ -198,24 +198,73 @@ public class MultiSelectManagerTest extends AndroidTestCase { provisional.append(2, true); provisional.append(2, true); s.setProvisionalSelection(getItemIds(provisional)); s.setProvisionalSelection(getItemIds(provisional)); assertSelection(items.get(1), items.get(2)); assertSelection(items.get(1), items.get(2)); } public void testProvisionalSelection_Replace() { Selection s = mManager.getSelection(); provisional.delete(1); SparseBooleanArray provisional = new SparseBooleanArray(); provisional.append(1, true); provisional.append(2, true); s.setProvisionalSelection(getItemIds(provisional)); provisional.clear(); provisional.append(3, true); provisional.append(3, true); provisional.append(4, true); s.setProvisionalSelection(getItemIds(provisional)); s.setProvisionalSelection(getItemIds(provisional)); assertSelection(items.get(2), items.get(3)); assertSelection(items.get(3), items.get(4)); } s.applyProvisionalSelection(); public void testProvisionalSelection_IntersectsExistingProvisionalSelection() { assertSelection(items.get(2), items.get(3)); Selection s = mManager.getSelection(); SparseBooleanArray provisional = new SparseBooleanArray(); provisional.append(1, true); provisional.append(2, true); s.setProvisionalSelection(getItemIds(provisional)); provisional.clear(); provisional.clear(); provisional.append(1, true); s.setProvisionalSelection(getItemIds(provisional)); assertSelection(items.get(1)); } public void testProvisionalSelection_Apply() { Selection s = mManager.getSelection(); SparseBooleanArray provisional = new SparseBooleanArray(); provisional.append(1, true); provisional.append(2, true); s.setProvisionalSelection(getItemIds(provisional)); s.applyProvisionalSelection(); assertSelection(items.get(1), items.get(2)); } public void testProvisionalSelection_Cancel() { mManager.toggleSelection(items.get(1)); mManager.toggleSelection(items.get(2)); Selection s = mManager.getSelection(); SparseBooleanArray provisional = new SparseBooleanArray(); provisional.append(3, true); provisional.append(3, true); provisional.append(4, true); provisional.append(4, true); s.setProvisionalSelection(getItemIds(provisional)); s.setProvisionalSelection(getItemIds(provisional)); assertSelection(items.get(2), items.get(3), items.get(4)); s.cancelProvisionalSelection(); // Original selection should remain. assertSelection(items.get(1), items.get(2)); } provisional.delete(3); public void testProvisionalSelection_IntersectsAppliedSelection() { mManager.toggleSelection(items.get(1)); mManager.toggleSelection(items.get(2)); Selection s = mManager.getSelection(); SparseBooleanArray provisional = new SparseBooleanArray(); provisional.append(2, true); provisional.append(3, true); s.setProvisionalSelection(getItemIds(provisional)); s.setProvisionalSelection(getItemIds(provisional)); assertSelection(items.get(2), items.get(3), items.get(4)); assertSelection(items.get(1), items.get(2), items.get(3)); } } private static Set<String> getItemIds(SparseBooleanArray selection) { private static Set<String> getItemIds(SparseBooleanArray selection) { Loading