Move selection to selection pkg, make activity scoped.
Move all other selection related classes to selection pkg.
Change-Id: I57a3964fada55b0f4d073f05a7833455235221b9
diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java
index 6a6d71c..9eb1359 100644
--- a/src/com/android/documentsui/AbstractActionHandler.java
+++ b/src/com/android/documentsui/AbstractActionHandler.java
@@ -35,11 +35,11 @@
import com.android.documentsui.dirlist.AnimationView.AnimationType;
import com.android.documentsui.dirlist.DocumentDetails;
import com.android.documentsui.dirlist.Model;
-import com.android.documentsui.dirlist.MultiSelectManager.Selection;
import com.android.documentsui.files.LauncherActivity;
import com.android.documentsui.files.OpenUriForViewTask;
import com.android.documentsui.roots.LoadRootTask;
import com.android.documentsui.roots.RootsAccess;
+import com.android.documentsui.selection.Selection;
import com.android.documentsui.sidebar.EjectRootTask;
import java.util.List;
diff --git a/src/com/android/documentsui/ActionHandler.java b/src/com/android/documentsui/ActionHandler.java
index 00aaee0..5ee3977 100644
--- a/src/com/android/documentsui/ActionHandler.java
+++ b/src/com/android/documentsui/ActionHandler.java
@@ -28,7 +28,7 @@
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.dirlist.DocumentDetails;
import com.android.documentsui.dirlist.Model;
-import com.android.documentsui.dirlist.MultiSelectManager.Selection;
+import com.android.documentsui.selection.Selection;
public interface ActionHandler {
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index e24794a..997a9bd 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -64,11 +64,13 @@
import com.android.documentsui.base.State.ViewMode;
import com.android.documentsui.dirlist.AnimationView;
import com.android.documentsui.dirlist.DirectoryFragment;
+import com.android.documentsui.dirlist.DocumentsAdapter;
import com.android.documentsui.dirlist.Model;
-import com.android.documentsui.dirlist.MultiSelectManager;
-import com.android.documentsui.dirlist.MultiSelectManager.Selection;
import com.android.documentsui.roots.GetRootDocumentTask;
import com.android.documentsui.roots.RootsCache;
+import com.android.documentsui.selection.SelectionManager;
+import com.android.documentsui.selection.SelectionManager.SelectionPredicate;
+import com.android.documentsui.selection.Selection;
import com.android.documentsui.sidebar.RootsFragment;
import com.android.documentsui.sorting.SortController;
import com.android.documentsui.sorting.SortModel;
@@ -96,12 +98,13 @@
protected SortController mSortController;
private final String mTag;
- private final ContentObserver mRootsCacheObserver = new ContentObserver(new Handler()) {
- @Override
- public void onChange(boolean selfChange) {
- new HandleRootsChangedTask(BaseActivity.this).execute(getCurrentRoot());
- }
- };
+ private final ContentObserver mRootsCacheObserver = new ContentObserver(
+ new Handler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ new HandleRootsChangedTask(BaseActivity.this).execute(getCurrentRoot());
+ }
+ };
@LayoutRes
private int mLayoutId;
@@ -125,6 +128,13 @@
* Provides Activity a means of injection into and specialization of
* DirectoryFragment.
*/
+ public abstract SelectionManager getSelectionManager(
+ DocumentsAdapter adapter, SelectionPredicate canSetState);
+
+ /**
+ * Provides Activity a means of injection into and specialization of
+ * DirectoryFragment.
+ */
public abstract FocusManager getFocusManager(RecyclerView view, Model model);
/**
@@ -146,7 +156,7 @@
* Args can be null when called from a context lacking fragment, such as RootsFragment.
*/
public abstract ActionHandler getActionHandler(
- @Nullable Model model, @Nullable MultiSelectManager selectionMgr, boolean searchMode);
+ @Nullable Model model, @Nullable SelectionManager selectionMgr, boolean searchMode);
public BaseActivity(@LayoutRes int layoutId, String tag) {
mLayoutId = layoutId;
diff --git a/src/com/android/documentsui/MenuManager.java b/src/com/android/documentsui/MenuManager.java
index 12c6217..5bf360f 100644
--- a/src/com/android/documentsui/MenuManager.java
+++ b/src/com/android/documentsui/MenuManager.java
@@ -17,7 +17,6 @@
package com.android.documentsui;
import android.app.Fragment;
-import android.view.KeyboardShortcutGroup;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -33,9 +32,6 @@
import com.android.documentsui.sidebar.RootsFragment;
import com.android.internal.annotations.VisibleForTesting;
-import java.util.List;
-import java.util.function.IntFunction;
-
public abstract class MenuManager {
final protected SearchViewManager mSearchManager;
diff --git a/src/com/android/documentsui/clipping/DocumentClipper.java b/src/com/android/documentsui/clipping/DocumentClipper.java
index fb453bc..f864fd1 100644
--- a/src/com/android/documentsui/clipping/DocumentClipper.java
+++ b/src/com/android/documentsui/clipping/DocumentClipper.java
@@ -31,7 +31,7 @@
import com.android.documentsui.base.DocumentStack;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.Shared;
-import com.android.documentsui.dirlist.MultiSelectManager.Selection;
+import com.android.documentsui.selection.Selection;
import com.android.documentsui.services.FileOperation;
import com.android.documentsui.services.FileOperationService;
import com.android.documentsui.services.FileOperationService.OpType;
diff --git a/src/com/android/documentsui/clipping/UrisSupplier.java b/src/com/android/documentsui/clipping/UrisSupplier.java
index f499653..98759b2 100644
--- a/src/com/android/documentsui/clipping/UrisSupplier.java
+++ b/src/com/android/documentsui/clipping/UrisSupplier.java
@@ -30,7 +30,7 @@
import com.android.documentsui.DocumentsApplication;
import com.android.documentsui.base.Shared;
-import com.android.documentsui.dirlist.MultiSelectManager.Selection;
+import com.android.documentsui.selection.Selection;
import com.android.documentsui.services.FileOperation;
import java.io.File;
diff --git a/src/com/android/documentsui/dirlist/ActionModeController.java b/src/com/android/documentsui/dirlist/ActionModeController.java
index 4e89cb1..6952d47 100644
--- a/src/com/android/documentsui/dirlist/ActionModeController.java
+++ b/src/com/android/documentsui/dirlist/ActionModeController.java
@@ -37,7 +37,8 @@
import com.android.documentsui.base.EventHandler;
import com.android.documentsui.base.Menus;
import com.android.documentsui.base.Shared;
-import com.android.documentsui.dirlist.MultiSelectManager.Selection;
+import com.android.documentsui.selection.Selection;
+import com.android.documentsui.selection.SelectionManager;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -46,12 +47,12 @@
/**
* A controller that listens to selection changes and manages life cycles of action modes.
*/
-public class ActionModeController implements MultiSelectManager.Callback, ActionMode.Callback {
+public class ActionModeController implements SelectionManager.Callback, ActionMode.Callback {
private static final String TAG = "ActionModeController";
private final Context mContext;
- private final MultiSelectManager mSelectionMgr;
+ private final SelectionManager mSelectionMgr;
private final MenuManager mMenuManager;
private final MenuManager.SelectionDetails mSelectionDetails;
@@ -68,7 +69,7 @@
private ActionModeController(
Context context,
- MultiSelectManager selectionMgr,
+ SelectionManager selectionMgr,
MenuManager menuManager,
MenuManager.SelectionDetails selectionDetails,
Function<ActionMode.Callback, ActionMode> actionModeFactory,
@@ -151,8 +152,16 @@
// Called when the user exits the action mode
@Override
public void onDestroyActionMode(ActionMode mode) {
+ if (mActionMode == null) {
+ if (DEBUG) Log.w(TAG, "Received call to destroy action mode on alien mode object.");
+ }
+
+ assert(mActionMode.equals(mode));
+
if (DEBUG) Log.d(TAG, "Handling action mode destroyed.");
mActionMode = null;
+ mMenu = null;
+
// clear selection
mSelectionMgr.clearSelection();
mSelected.clear();
@@ -202,7 +211,7 @@
static ActionModeController create(
Context context,
- MultiSelectManager selectionMgr,
+ SelectionManager selectionMgr,
MenuManager menuManager,
MenuManager.SelectionDetails selectionDetails,
Activity activity,
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 8ba2efb..e7ab4b1 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -89,9 +89,13 @@
import com.android.documentsui.clipping.DocumentClipper;
import com.android.documentsui.clipping.UrisSupplier;
import com.android.documentsui.dirlist.AnimationView.AnimationType;
-import com.android.documentsui.dirlist.MultiSelectManager.Selection;
import com.android.documentsui.picker.PickActivity;
import com.android.documentsui.roots.RootsAccess;
+import com.android.documentsui.selection.BandController;
+import com.android.documentsui.selection.GestureSelector;
+import com.android.documentsui.selection.Selection;
+import com.android.documentsui.selection.SelectionManager;
+import com.android.documentsui.selection.SelectionMetadata;
import com.android.documentsui.services.FileOperation;
import com.android.documentsui.services.FileOperationService;
import com.android.documentsui.services.FileOperationService.OpType;
@@ -146,6 +150,9 @@
private ActivityConfig mActivityConfig;
// This dependency is informally "injected" from the owning Activity in our onCreate method.
+ private SelectionManager mSelectionMgr;
+
+ // This dependency is informally "injected" from the owning Activity in our onCreate method.
private FocusManager mFocusManager;
// This dependency is informally "injected" from the owning Activity in our onCreate method.
@@ -157,7 +164,6 @@
// This dependency is informally "injected" from the owning Activity in our onCreate method.
private DialogController mDialogs;
- private MultiSelectManager mSelectionMgr;
private ActionModeController mActionModeController;
private SelectionMetadata mSelectionMetadata;
private UserInputHandler<InputEvent> mInputHandler;
@@ -286,31 +292,33 @@
}
mRecView.setLayoutManager(mLayout);
- // 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
- // into the selection manager.
- mSelectionMgr = new MultiSelectManager(
- mAdapter,
- state.allowMultiple
- ? MultiSelectManager.MODE_MULTIPLE
- : MultiSelectManager.MODE_SINGLE,
- this::canSetSelectionState);
- mSelectionMetadata = new SelectionMetadata(mModel::getItem);
- mSelectionMgr.addItemCallback(mSelectionMetadata);
-
mModel.addUpdateListener(mAdapter.getModelUpdateListener());
mModel.addUpdateListener(mModelUpdateListener);
- GestureSelector gestureSel = GestureSelector.create(mSelectionMgr, mRecView);
final BaseActivity activity = getBaseActivity();
+ mSelectionMgr = activity.getSelectionManager(mAdapter, this::canSetSelectionState);
mFocusManager = activity.getFocusManager(mRecView, mModel);
mActions = activity.getActionHandler(mModel, mSelectionMgr, mConfig.mSearchMode);
mMenuManager = activity.getMenuManager();
mDialogs = activity.getDialogController();
+ mSelectionMetadata = new SelectionMetadata(mModel::getItem);
+ mSelectionMgr.addItemCallback(mSelectionMetadata);
+
+ GestureSelector gestureSel = GestureSelector.create(mSelectionMgr, mRecView);
+
if (state.allowMultiple) {
- mBandController = new BandController(mRecView, mAdapter, mSelectionMgr);
+ mBandController = new BandController(
+ mRecView,
+ mAdapter,
+ mSelectionMgr,
+ (int pos) -> {
+ // The band selection model only operates on documents and directories.
+ // Exclude other types of adapter items like whitespace and dividers.
+ RecyclerView.ViewHolder vh = mRecView.findViewHolderForAdapterPosition(pos);
+ return ModelBackedDocumentsAdapter.isContentType(vh.getItemViewType());
+ });
}
DragStartListener mDragStartListener = mActivityConfig.dragAndDropEnabled()
@@ -1171,10 +1179,12 @@
}
}
+ // TODO: Move to activities when Model becomes activity level object.
private boolean canSelect(DocumentDetails doc) {
return canSetSelectionState(doc.getModelId(), true);
}
+ // TODO: Move to activities when Model becomes activity level object.
private boolean canSetSelectionState(String modelId, boolean nextState) {
if (nextState) {
// Check if an item can be selected
diff --git a/src/com/android/documentsui/dirlist/DocumentsAdapter.java b/src/com/android/documentsui/dirlist/DocumentsAdapter.java
index a48c66a..04baa50 100644
--- a/src/com/android/documentsui/dirlist/DocumentsAdapter.java
+++ b/src/com/android/documentsui/dirlist/DocumentsAdapter.java
@@ -56,7 +56,7 @@
* Triggers item-change notifications by stable ID (as opposed to position).
* Passing an unrecognized ID will result in a warning in logcat, but no other error.
*/
- abstract void onItemSelectionChanged(String id);
+ public abstract void onItemSelectionChanged(String id);
/**
* @return The model ID of the item at the given adapter position.
diff --git a/src/com/android/documentsui/dirlist/DragHoverListener.java b/src/com/android/documentsui/dirlist/DragHoverListener.java
index 6b694e1..63d93ec 100644
--- a/src/com/android/documentsui/dirlist/DragHoverListener.java
+++ b/src/com/android/documentsui/dirlist/DragHoverListener.java
@@ -24,8 +24,9 @@
import com.android.documentsui.ItemDragListener;
import com.android.documentsui.ItemDragListener.DragHost;
-import com.android.documentsui.dirlist.ViewAutoScroller.ScrollActionDelegate;
-import com.android.documentsui.dirlist.ViewAutoScroller.ScrollDistanceDelegate;
+import com.android.documentsui.ui.ViewAutoScroller;
+import com.android.documentsui.ui.ViewAutoScroller.ScrollActionDelegate;
+import com.android.documentsui.ui.ViewAutoScroller.ScrollDistanceDelegate;
import java.util.function.BooleanSupplier;
import java.util.function.IntSupplier;
diff --git a/src/com/android/documentsui/dirlist/DragShadowBuilder.java b/src/com/android/documentsui/dirlist/DragShadowBuilder.java
index d32ede8..0092f6c 100644
--- a/src/com/android/documentsui/dirlist/DragShadowBuilder.java
+++ b/src/com/android/documentsui/dirlist/DragShadowBuilder.java
@@ -29,7 +29,7 @@
import com.android.documentsui.R;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.Shared;
-import com.android.documentsui.dirlist.MultiSelectManager.Selection;
+import com.android.documentsui.selection.Selection;
import java.util.List;
import java.util.function.Function;
diff --git a/src/com/android/documentsui/dirlist/DragStartListener.java b/src/com/android/documentsui/dirlist/DragStartListener.java
index c22df34..2bca5b2 100644
--- a/src/com/android/documentsui/dirlist/DragStartListener.java
+++ b/src/com/android/documentsui/dirlist/DragStartListener.java
@@ -30,7 +30,8 @@
import com.android.documentsui.base.State;
import com.android.documentsui.base.Events.InputEvent;
import com.android.documentsui.clipping.DocumentClipper;
-import com.android.documentsui.dirlist.MultiSelectManager.Selection;
+import com.android.documentsui.selection.SelectionManager;
+import com.android.documentsui.selection.Selection;
import com.android.documentsui.services.FileOperationService;
import com.android.documentsui.services.FileOperationService.OpType;
@@ -66,7 +67,7 @@
private static String TAG = "DragStartListener";
private final State mState;
- private final MultiSelectManager mSelectionMgr;
+ private final SelectionManager mSelectionMgr;
private final ViewFinder mViewFinder;
private final Function<View, String> mIdFinder;
private final ClipDataFactory mClipFactory;
@@ -77,7 +78,7 @@
@VisibleForTesting
public ActiveListener(
State state,
- MultiSelectManager selectionMgr,
+ SelectionManager selectionMgr,
ViewFinder viewFinder,
Function<View, String> idFinder,
Function<Selection, List<DocumentInfo>> docsConverter,
@@ -176,7 +177,7 @@
IconHelper iconHelper,
Context context,
Model model,
- MultiSelectManager selectionMgr,
+ SelectionManager selectionMgr,
DocumentClipper clipper,
State state,
Function<View, String> idFinder,
diff --git a/src/com/android/documentsui/dirlist/ListeningGestureDetector.java b/src/com/android/documentsui/dirlist/ListeningGestureDetector.java
index a810244..296fa70 100644
--- a/src/com/android/documentsui/dirlist/ListeningGestureDetector.java
+++ b/src/com/android/documentsui/dirlist/ListeningGestureDetector.java
@@ -29,6 +29,8 @@
import com.android.documentsui.base.Events;
import com.android.documentsui.base.Events.InputEvent;
import com.android.documentsui.base.Events.MotionInputEvent;
+import com.android.documentsui.selection.BandController;
+import com.android.documentsui.selection.GestureSelector;
//Receives event meant for both directory and empty view, and either pass them to
//{@link UserInputHandler} for simple gestures (Single Tap, Long-Press), or intercept them for
diff --git a/src/com/android/documentsui/dirlist/Model.java b/src/com/android/documentsui/dirlist/Model.java
index 6c08859..016346a 100644
--- a/src/com/android/documentsui/dirlist/Model.java
+++ b/src/com/android/documentsui/dirlist/Model.java
@@ -33,8 +33,8 @@
import com.android.documentsui.DirectoryResult;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.EventListener;
-import com.android.documentsui.dirlist.MultiSelectManager.Selection;
import com.android.documentsui.roots.RootCursorWrapper;
+import com.android.documentsui.selection.Selection;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
diff --git a/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java b/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java
index c1077c5..adc4b04 100644
--- a/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java
+++ b/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java
@@ -192,6 +192,19 @@
: ITEM_TYPE_DOCUMENT;
}
+ /**
+ * @return true if the item type is either document or directory, false for all other
+ * possible types.
+ */
+ public static boolean isContentType(int type) {
+ switch (type) {
+ case ModelBackedDocumentsAdapter.ITEM_TYPE_DOCUMENT:
+ case ModelBackedDocumentsAdapter.ITEM_TYPE_DIRECTORY:
+ return true;
+ }
+ return false;
+ }
+
@Override
public void onItemSelectionChanged(String id) {
int position = mModelIds.indexOf(id);
diff --git a/src/com/android/documentsui/dirlist/MultiSelectManager.java b/src/com/android/documentsui/dirlist/MultiSelectManager.java
deleted file mode 100644
index 168134a..0000000
--- a/src/com/android/documentsui/dirlist/MultiSelectManager.java
+++ /dev/null
@@ -1,882 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.documentsui.dirlist;
-
-import static com.android.documentsui.base.Shared.DEBUG;
-
-import android.annotation.IntDef;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.support.annotation.VisibleForTesting;
-import android.support.v7.widget.RecyclerView;
-import android.util.Log;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import javax.annotation.Nullable;
-
-/**
- * MultiSelectManager provides support traditional multi-item selection support to RecyclerView.
- * Additionally it can be configured to restrict selection to a single element, @see
- * #setSelectMode.
- */
-public final class MultiSelectManager {
-
- @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_SINGLE = 1;
-
- @IntDef({
- RANGE_REGULAR,
- RANGE_PROVISIONAL
- })
- @Retention(RetentionPolicy.SOURCE)
- public @interface RangeType {}
- public static final int RANGE_REGULAR = 0;
- public static final int RANGE_PROVISIONAL = 1;
-
- private static final String TAG = "MultiSelectManager";
-
- private final Selection mSelection = new Selection();
-
- private final DocumentsAdapter mAdapter;
- private final List<Callback> mCallbacks = new ArrayList<>(1);
- private final List<ItemCallback> mItemCallbacks = new ArrayList<>(1);
-
- private @Nullable Range mRanger;
- private boolean mSingleSelect;
-
- private final SelectionPredicate mCanSetState;
-
- public MultiSelectManager(
- DocumentsAdapter adapter,
- @SelectionMode int mode,
- SelectionPredicate canSetState) {
-
- assert(adapter != null);
-
- mAdapter = adapter;
-
- mCanSetState = canSetState;
-
- mSingleSelect = mode == MODE_SINGLE;
- mAdapter.registerAdapterDataObserver(
- new RecyclerView.AdapterDataObserver() {
-
- private List<String> mModelIds;
-
- @Override
- public void onChanged() {
- mModelIds = mAdapter.getModelIds();
-
- // Update the selection to remove any disappeared IDs.
- mSelection.cancelProvisionalSelection();
- mSelection.intersect(mModelIds);
- }
-
- @Override
- public void onItemRangeChanged(
- int startPosition, int itemCount, Object payload) {
- // No change in position. Ignoring.
- }
-
- @Override
- public void onItemRangeInserted(int startPosition, int itemCount) {
- mSelection.cancelProvisionalSelection();
- }
-
- @Override
- public void onItemRangeRemoved(int startPosition, int itemCount) {
- assert(startPosition >= 0);
- assert(itemCount > 0);
-
- mSelection.cancelProvisionalSelection();
- // Remove any disappeared IDs from the selection.
- mSelection.intersect(mModelIds);
- }
-
- @Override
- public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
- throw new UnsupportedOperationException();
- }
- });
- }
-
- void bindContoller(BandController controller) {
- // Provides BandController with access to private mSelection state.
- controller.bindSelection(mSelection);
- }
-
- /**
- * Adds {@code callback} such that it will be notified when {@code MultiSelectManager}
- * events occur.
- *
- * @param callback
- */
- public void addCallback(Callback callback) {
- assert(callback != null);
- mCallbacks.add(callback);
- }
-
- public void addItemCallback(ItemCallback itemCallback) {
- assert(itemCallback != null);
- mItemCallbacks.add(itemCallback);
- }
-
- public boolean hasSelection() {
- return !mSelection.isEmpty();
- }
-
- /**
- * Returns a Selection object that provides a live view
- * on the current selection.
- *
- * @see #getSelection(Selection) on how to get a snapshot
- * of the selection that will not reflect future changes
- * to selection.
- *
- * @return The current selection.
- */
- public Selection getSelection() {
- return mSelection;
- }
-
- /**
- * Updates {@code dest} to reflect the current selection.
- * @param dest
- *
- * @return The Selection instance passed in, for convenience.
- */
- public Selection getSelection(Selection dest) {
- dest.copyFrom(mSelection);
- return dest;
- }
-
- public void replaceSelection(Iterable<String> ids) {
- clearSelection();
- setItemsSelected(ids, true);
- }
-
- /**
- * Restores the selected state of specified items. Used in cases such as restore the selection
- * after rotation etc.
- */
- public void restoreSelection(Selection other) {
- setItemsSelectedQuietly(other.mSelection, true);
- // NOTE: We intentionally don't restore provisional selection. It's provisional.
- notifySelectionRestored();
- }
-
- /**
- * Sets the selected state of the specified items. Note that the callback will NOT
- * be consulted to see if an item can be selected.
- *
- * @param ids
- * @param selected
- * @return
- */
- public boolean setItemsSelected(Iterable<String> ids, boolean selected) {
- final boolean changed = setItemsSelectedQuietly(ids, selected);
- notifySelectionChanged();
- return changed;
- }
-
- private boolean setItemsSelectedQuietly(Iterable<String> ids, boolean selected) {
- boolean changed = false;
- for (String id: ids) {
- final boolean itemChanged =
- selected
- ? canSetState(id, true) && mSelection.add(id)
- : canSetState(id, false) && mSelection.remove(id);
- if (itemChanged) {
- notifyItemStateChanged(id, selected);
- }
- changed |= itemChanged;
- }
- return changed;
- }
-
- /**
- * Clears the selection and notifies (even if nothing changes).
- */
- public void clearSelection() {
- clearSelectionQuietly();
- notifySelectionChanged();
- }
-
- /**
- * Clears the selection, without notifying selection listeners. UI elements still need to be
- * notified about state changes so that they can update their appearance.
- */
- private void clearSelectionQuietly() {
- mRanger = null;
-
- if (!hasSelection()) {
- return;
- }
-
- Selection oldSelection = getSelection(new Selection());
- mSelection.clear();
-
- for (String id: oldSelection.mSelection) {
- notifyItemStateChanged(id, false);
- }
- for (String id: oldSelection.mProvisionalSelection) {
- notifyItemStateChanged(id, false);
- }
- }
-
- /**
- * Toggles selection on the item with the given model ID.
- *
- * @param modelId
- */
- public void toggleSelection(String modelId) {
- assert(modelId != null);
-
- final boolean changed =
- mSelection.contains(modelId)
- ? attemptDeselect(modelId)
- : attemptSelect(modelId);
-
- if (changed) {
- notifySelectionChanged();
- }
- }
-
- /**
- * Starts a range selection. If a range selection is already active, this will start a new range
- * selection (which will reset the range anchor).
- *
- * @param pos The anchor position for the selection range.
- */
- void startRangeSelection(int pos) {
- attemptSelect(mAdapter.getModelId(pos));
- setSelectionRangeBegin(pos);
- }
-
- void snapRangeSelection(int pos) {
- snapRangeSelection(pos, RANGE_REGULAR);
- }
-
- void snapProvisionalRangeSelection(int pos) {
- snapRangeSelection(pos, RANGE_PROVISIONAL);
- }
-
- /**
- * Sets the end point for the current range selection, started by a call to
- * {@link #startRangeSelection(int)}. This function should only be called when a range selection
- * is active (see {@link #isRangeSelectionActive()}. Items in the range [anchor, end] will be
- * selected or in provisional select, depending on the type supplied. Note that if the type is
- * provisional select, one should do {@link Selection#applyProvisionalSelection()} at some point
- * before calling on {@link #endRangeSelection()}.
- *
- * @param pos The new end position for the selection range.
- * @param type The type of selection the range should utilize.
- */
- private void snapRangeSelection(int pos, @RangeType int type) {
- if (!isRangeSelectionActive()) {
- throw new IllegalStateException("Range start point not set.");
- }
-
- mRanger.snapSelection(pos, type);
-
- // We're being lazy here notifying even when something might not have changed.
- // To make this more correct, we'd need to update the Ranger class to return
- // information about what has changed.
- notifySelectionChanged();
- }
-
- /**
- * Stops an in-progress range selection. All selection done with
- * {@link #snapRangeSelection(int, int)} with type RANGE_PROVISIONAL will be lost if
- * {@link Selection#applyProvisionalSelection()} is not called beforehand.
- */
- void endRangeSelection() {
- mRanger = null;
- // Clean up in case there was any leftover provisional selection
- mSelection.cancelProvisionalSelection();
- }
-
- /**
- * @return Whether or not there is a current range selection active.
- */
- boolean isRangeSelectionActive() {
- return mRanger != null;
- }
-
- /**
- * Sets the magic location at which a selection range begins (the selection anchor). This value
- * is consulted when determining how to extend, and modify selection ranges. Calling this when a
- * range selection is active will reset the range selection.
- */
- void setSelectionRangeBegin(int position) {
- if (position == RecyclerView.NO_POSITION) {
- return;
- }
-
- if (mSelection.contains(mAdapter.getModelId(position))) {
- mRanger = new Range(this::updateForRange, position);
- }
- }
-
- /**
- * @param modelId
- * @return True if the update was applied.
- */
- private boolean selectAndNotify(String modelId) {
- boolean changed = mSelection.add(modelId);
- if (changed) {
- notifyItemStateChanged(modelId, true);
- }
- return changed;
- }
-
- /**
- * @param id
- * @return True if the update was applied.
- */
- private boolean attemptDeselect(String id) {
- assert(id != null);
- if (canSetState(id, false)) {
- mSelection.remove(id);
- notifyItemStateChanged(id, false);
- if (DEBUG) Log.d(TAG, "Selection after deselect: " + mSelection);
- return true;
- } else {
- if (DEBUG) Log.d(TAG, "Select cancelled by listener.");
- return false;
- }
- }
-
- /**
- * @param id
- * @return True if the update was applied.
- */
- private boolean attemptSelect(String id) {
- assert(id != null);
- boolean canSelect = canSetState(id, true);
- if (!canSelect) {
- return false;
- }
- if (mSingleSelect && hasSelection()) {
- clearSelectionQuietly();
- }
-
- selectAndNotify(id);
- return true;
- }
-
- boolean canSetState(String id, boolean nextState) {
- return mCanSetState.test(id, nextState);
- }
-
- /**
- * Notifies registered listeners when the selection status of a single item
- * (identified by {@code position}) changes.
- */
- void notifyItemStateChanged(String id, boolean selected) {
- assert(id != null);
- int lastListener = mItemCallbacks.size() - 1;
- for (int i = lastListener; i >= 0; i--) {
- mItemCallbacks.get(i).onItemStateChanged(id, selected);
- }
- mAdapter.onItemSelectionChanged(id);
- }
-
- /**
- * Notifies registered listeners when the selection has changed. This
- * notification should be sent only once a full series of changes
- * is complete, e.g. clearingSelection, or updating the single
- * selection from one item to another.
- */
- void notifySelectionChanged() {
- int lastListener = mCallbacks.size() - 1;
- for (int i = lastListener; i > -1; i--) {
- mCallbacks.get(i).onSelectionChanged();
- }
- }
-
- private void notifySelectionRestored() {
- int lastListener = mCallbacks.size() - 1;
- for (int i = lastListener; i > -1; i--) {
- mCallbacks.get(i).onSelectionRestored();
- }
- }
-
- private void updateForRange(int begin, int end, boolean selected, @RangeType int type) {
- switch (type) {
- case RANGE_REGULAR:
- updateForRegularRange(begin, end, selected);
- break;
- case RANGE_PROVISIONAL:
- updateForProvisionalRange(begin, end, selected);
- break;
- default:
- throw new IllegalArgumentException("Invalid range type: " + type);
- }
- }
-
- private void updateForRegularRange(int begin, int end, boolean selected) {
- assert(end >= begin);
- for (int i = begin; i <= end; i++) {
- String id = mAdapter.getModelId(i);
- if (id == null) {
- continue;
- }
-
- if (selected) {
- boolean canSelect = canSetState(id, true);
- if (canSelect) {
- if (mSingleSelect && hasSelection()) {
- clearSelectionQuietly();
- }
- selectAndNotify(id);
- }
- } else {
- attemptDeselect(id);
- }
- }
- }
-
- private void updateForProvisionalRange(int begin, int end, boolean selected) {
- assert (end >= begin);
- for (int i = begin; i <= end; i++) {
- String id = mAdapter.getModelId(i);
- if (id == null) {
- continue;
- }
- if (selected) {
- boolean canSelect = canSetState(id, true);
- if (canSelect) {
- mSelection.mProvisionalSelection.add(id);
- }
- } else {
- mSelection.mProvisionalSelection.remove(id);
- }
- notifyItemStateChanged(id, selected);
- }
- notifySelectionChanged();
- }
-
- /**
- * Class providing support for managing range selections.
- */
- private static final class Range {
- private static final int UNDEFINED = -1;
-
- private final RangeUpdater mUpdater;
- private final int mBegin;
- private int mEnd = UNDEFINED;
-
- public Range(RangeUpdater updater, int begin) {
- if (DEBUG) Log.d(TAG, "New Ranger created beginning @ " + begin);
- mUpdater = updater;
- mBegin = begin;
- }
-
- private void snapSelection(int position, @RangeType int type) {
- assert(position != RecyclerView.NO_POSITION);
-
- if (mEnd == UNDEFINED || mEnd == mBegin) {
- // Reset mEnd so it can be established in establishRange.
- mEnd = UNDEFINED;
- establishRange(position, type);
- } else {
- reviseRange(position, type);
- }
- }
-
- private void establishRange(int position, @RangeType int type) {
- assert(mEnd == UNDEFINED);
-
- if (position == mBegin) {
- mEnd = position;
- }
-
- if (position > mBegin) {
- updateRange(mBegin + 1, position, true, type);
- } else if (position < mBegin) {
- updateRange(position, mBegin - 1, true, type);
- }
-
- mEnd = position;
- }
-
- private void reviseRange(int position, @RangeType int type) {
- assert(mEnd != UNDEFINED);
- assert(mBegin != mEnd);
-
- if (position == mEnd) {
- if (DEBUG) Log.v(TAG, "Ignoring no-op revision for range: " + this);
- }
-
- if (mEnd > mBegin) {
- reviseAscendingRange(position, type);
- } else if (mEnd < mBegin) {
- reviseDescendingRange(position, type);
- }
- // the "else" case is covered by checkState at beginning of method.
-
- mEnd = position;
- }
-
- /**
- * Updates an existing ascending seleciton.
- * @param position
- */
- private void reviseAscendingRange(int position, @RangeType int type) {
- // Reducing or reversing the range....
- if (position < mEnd) {
- if (position < mBegin) {
- updateRange(mBegin + 1, mEnd, false, type);
- updateRange(position, mBegin -1, true, type);
- } else {
- updateRange(position + 1, mEnd, false, type);
- }
- }
-
- // Extending the range...
- else if (position > mEnd) {
- updateRange(mEnd + 1, position, true, type);
- }
- }
-
- private void reviseDescendingRange(int position, @RangeType int type) {
- // Reducing or reversing the range....
- if (position > mEnd) {
- if (position > mBegin) {
- updateRange(mEnd, mBegin - 1, false, type);
- updateRange(mBegin + 1, position, true, type);
- } else {
- updateRange(mEnd, position - 1, false, type);
- }
- }
-
- // Extending the range...
- else if (position < mEnd) {
- updateRange(position, mEnd - 1, true, type);
- }
- }
-
- /**
- * Try to set selection state for all elements in range. Not that callbacks can cancel
- * selection of specific items, so some or even all items may not reflect the desired state
- * after the update is complete.
- *
- * @param begin Adapter position for range start (inclusive).
- * @param end Adapter position for range end (inclusive).
- * @param selected New selection state.
- */
- private void updateRange(int begin, int end, boolean selected, @RangeType int type) {
- mUpdater.updateForRange(begin, end, selected, type);
- }
-
- @Override
- public String toString() {
- return "Range{begin=" + mBegin + ", end=" + mEnd + "}";
- }
- /*
- * @see {@link MultiSelectManager#updateForRegularRange(int, int , boolean)} and {@link
- * MultiSelectManager#updateForProvisionalRange(int, int, boolean)}
- */
- @FunctionalInterface
- private interface RangeUpdater {
- void updateForRange(int begin, int end, boolean selected, @RangeType int type);
- }
- }
-
- /**
- * Object representing the current selection. Provides read only access
- * public access, and private write access.
- */
- public static final class Selection implements Iterable<String>, Parcelable {
-
- // 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
- // completing a band select operation. Provisional selections are selections which have been
- // temporarily created by an in-progress band select operation (once the user releases the
- // mouse button during a band select operation, the selected items become saved). The total
- // selection is the combination of both the saved selection and the provisional
- // selection. Tracking both separately is necessary to ensure that saved selections do not
- // become deselected when they are removed from the provisional selection; for example, if
- // 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
- // selection must be tracked separately.
- private final Set<String> mSelection;
- private final Set<String> mProvisionalSelection;
-
- public Selection() {
- mSelection = new HashSet<>();
- mProvisionalSelection = new HashSet<>();
- }
-
- /**
- * Used by CREATOR.
- */
- private Selection(Set<String> selection) {
- mSelection = selection;
- mProvisionalSelection = new HashSet<>();
- }
-
- /**
- * @param id
- * @return true if the position is currently selected.
- */
- public boolean contains(@Nullable String id) {
- return mSelection.contains(id) || mProvisionalSelection.contains(id);
- }
-
- /**
- * Returns an {@link Iterator} that iterators over the selection, *excluding*
- * any provisional selection.
- *
- * {@inheritDoc}
- */
- @Override
- public Iterator<String> iterator() {
- return mSelection.iterator();
- }
-
- /**
- * @return size of the selection including both final and provisional selected items.
- */
- public int size() {
- return mSelection.size() + mProvisionalSelection.size();
- }
-
- /**
- * @return true if the selection is empty.
- */
- public boolean isEmpty() {
- return mSelection.isEmpty() && mProvisionalSelection.isEmpty();
- }
-
- /**
- * 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
- * one (if it exists) is abandoned.
- * @return Map of ids added or removed. Added ids have a value of true, removed are false.
- */
- @VisibleForTesting
- protected Map<String, Boolean> setProvisionalSelection(Set<String> newSelection) {
- Map<String, Boolean> delta = new HashMap<>();
-
- 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
- // provisional selection.
- if (!newSelection.contains(id)) {
- delta.put(id, false);
- }
- }
-
- for (String id: newSelection) {
- // Mark each item that was not previously in the selection but is in the new
- // provisional selection.
- if (!mSelection.contains(id) && !mProvisionalSelection.contains(id)) {
- delta.put(id, true);
- }
- }
-
- // Now, iterate through the changes and actually add/remove them to/from the current
- // selection. This could not be done in the previous loops because changing the size of
- // the selection mid-iteration changes iteration order erroneously.
- for (Map.Entry<String, Boolean> entry: delta.entrySet()) {
- String id = entry.getKey();
- if (entry.getValue()) {
- mProvisionalSelection.add(id);
- } else {
- mProvisionalSelection.remove(id);
- }
- }
-
- return delta;
- }
-
- /**
- * Saves the existing provisional selection. Once the provisional selection is saved,
- * subsequent provisional selections which are different from this existing one cannot
- * cause items in this existing provisional selection to become deselected.
- */
- @VisibleForTesting
- protected void applyProvisionalSelection() {
- mSelection.addAll(mProvisionalSelection);
- mProvisionalSelection.clear();
- }
-
- /**
- * Abandons the existing provisional selection so that all items provisionally selected are
- * now deselected.
- */
- @VisibleForTesting
- void cancelProvisionalSelection() {
- mProvisionalSelection.clear();
- }
-
- /** @hide */
- @VisibleForTesting
- public boolean add(String id) {
- if (!mSelection.contains(id)) {
- mSelection.add(id);
- return true;
- }
- return false;
- }
-
- /** @hide */
- @VisibleForTesting
- boolean remove(String id) {
- if (mSelection.contains(id)) {
- mSelection.remove(id);
- return true;
- }
- return false;
- }
-
- public void clear() {
- mSelection.clear();
- }
-
- /**
- * Trims this selection to be the intersection of itself with the set of given IDs.
- */
- public void intersect(Collection<String> ids) {
- mSelection.retainAll(ids);
- mProvisionalSelection.retainAll(ids);
- }
-
- @VisibleForTesting
- void copyFrom(Selection source) {
- mSelection.clear();
- mSelection.addAll(source.mSelection);
-
- mProvisionalSelection.clear();
- mProvisionalSelection.addAll(source.mProvisionalSelection);
- }
-
- @Override
- public String toString() {
- if (size() <= 0) {
- return "size=0, items=[]";
- }
-
- StringBuilder buffer = new StringBuilder(size() * 28);
- buffer.append("Selection{")
- .append("applied{size=" + mSelection.size())
- .append(", entries=" + mSelection)
- .append("}, provisional{size=" + mProvisionalSelection.size())
- .append(", entries=" + mProvisionalSelection)
- .append("}}");
- return buffer.toString();
- }
-
- @Override
- public int hashCode() {
- return mSelection.hashCode() ^ mProvisionalSelection.hashCode();
- }
-
- @Override
- public boolean equals(Object that) {
- if (this == that) {
- return true;
- }
-
- if (!(that instanceof Selection)) {
- return false;
- }
-
- return mSelection.equals(((Selection) that).mSelection) &&
- mProvisionalSelection.equals(((Selection) that).mProvisionalSelection);
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeStringList(new ArrayList<>(mSelection));
- // We don't include provisional selection since it is
- // typically coupled to some other runtime state (like a band).
- }
-
- public static final ClassLoaderCreator<Selection> CREATOR =
- new ClassLoaderCreator<Selection>() {
- @Override
- public Selection createFromParcel(Parcel in) {
- return createFromParcel(in, null);
- }
-
- @Override
- public Selection createFromParcel(Parcel in, ClassLoader loader) {
- ArrayList<String> selected = new ArrayList<>();
- in.readStringList(selected);
-
- return new Selection(new HashSet<>(selected));
- }
-
- @Override
- public Selection[] newArray(int size) {
- return new Selection[size];
- }
- };
- }
-
- public interface ItemCallback {
- void onItemStateChanged(String id, boolean selected);
- }
-
- public interface Callback {
- /**
- * Called immediately after completion of any set of changes.
- */
- void onSelectionChanged();
-
- /**
- * Called immediately after selection is restored.
- */
- void onSelectionRestored();
- }
-
- @FunctionalInterface
- public interface SelectionPredicate {
- boolean test(String id, boolean nextState);
- }
-}
diff --git a/src/com/android/documentsui/dirlist/UserInputHandler.java b/src/com/android/documentsui/dirlist/UserInputHandler.java
index cc9a81f..4af1fcd 100644
--- a/src/com/android/documentsui/dirlist/UserInputHandler.java
+++ b/src/com/android/documentsui/dirlist/UserInputHandler.java
@@ -28,6 +28,7 @@
import com.android.documentsui.base.EventHandler;
import com.android.documentsui.base.Events;
import com.android.documentsui.base.Events.InputEvent;
+import com.android.documentsui.selection.SelectionManager;
import java.util.Collections;
import java.util.function.Function;
@@ -46,7 +47,7 @@
private ActionHandler mActionHandler;
private final FocusHandler mFocusHandler;
- private final MultiSelectManager mSelectionMgr;
+ private final SelectionManager mSelectionMgr;
private final Function<MotionEvent, T> mEventConverter;
private final Predicate<DocumentDetails> mSelectable;
@@ -63,7 +64,7 @@
public UserInputHandler(
ActionHandler actionHandler,
FocusHandler focusHandler,
- MultiSelectManager selectionMgr,
+ SelectionManager selectionMgr,
Function<MotionEvent, T> eventConverter,
Predicate<DocumentDetails> selectable,
EventHandler<InputEvent> rightClickHandler,
diff --git a/src/com/android/documentsui/files/ActionHandler.java b/src/com/android/documentsui/files/ActionHandler.java
index 6a1f3ad..7e94a29 100644
--- a/src/com/android/documentsui/files/ActionHandler.java
+++ b/src/com/android/documentsui/files/ActionHandler.java
@@ -46,11 +46,13 @@
import com.android.documentsui.dirlist.DocumentDetails;
import com.android.documentsui.dirlist.Model;
import com.android.documentsui.dirlist.Model.Update;
-import com.android.documentsui.dirlist.MultiSelectManager;
-import com.android.documentsui.dirlist.MultiSelectManager.Selection;
+import com.android.documentsui.selection.Selection;
+import com.android.documentsui.selection.SelectionManager;
import com.android.documentsui.files.ActionHandler.Addons;
import com.android.documentsui.roots.GetRootDocumentTask;
import com.android.documentsui.roots.RootsAccess;
+import com.android.documentsui.selection.SelectionManager;
+import com.android.documentsui.selection.Selection;
import com.android.documentsui.services.FileOperation;
import com.android.documentsui.services.FileOperationService;
import com.android.documentsui.services.FileOperations;
@@ -466,7 +468,7 @@
mConfig.modelLoadObserved = true;
}
- ActionHandler<T> reset(Model model, MultiSelectManager selectionMgr, boolean searchMode) {
+ ActionHandler<T> reset(Model model, SelectionManager selectionMgr, boolean searchMode) {
mConfig.reset(model, selectionMgr, searchMode);
return this;
}
@@ -474,7 +476,7 @@
private static final class Config {
@Nullable Model model;
- @Nullable MultiSelectManager selectionMgr;
+ @Nullable SelectionManager selectionMgr;
boolean searchMode;
private final EventListener<Update> mModelUpdateListener;
@@ -487,7 +489,7 @@
mModelUpdateListener = modelUpdateListener;
}
- public void reset(Model model, MultiSelectManager selectionMgr, boolean searchMode) {
+ public void reset(Model model, SelectionManager selectionMgr, boolean searchMode) {
assert(model != null);
this.model = model;
diff --git a/src/com/android/documentsui/files/FilesActivity.java b/src/com/android/documentsui/files/FilesActivity.java
index 4feb5f1..0de0e26 100644
--- a/src/com/android/documentsui/files/FilesActivity.java
+++ b/src/com/android/documentsui/files/FilesActivity.java
@@ -50,8 +50,10 @@
import com.android.documentsui.ActivityConfig;
import com.android.documentsui.dirlist.AnimationView.AnimationType;
import com.android.documentsui.dirlist.DirectoryFragment;
+import com.android.documentsui.dirlist.DocumentsAdapter;
import com.android.documentsui.dirlist.Model;
-import com.android.documentsui.dirlist.MultiSelectManager;
+import com.android.documentsui.selection.SelectionManager;
+import com.android.documentsui.selection.SelectionManager.SelectionPredicate;
import com.android.documentsui.services.FileOperationService;
import com.android.documentsui.sidebar.RootsFragment;
import com.android.documentsui.ui.DialogController;
@@ -68,6 +70,7 @@
public static final String TAG = "FilesActivity";
private final Config mConfig = new Config();
+ private SelectionManager mSelectionMgr;
private MenuManager mMenuManager;
private FocusManager mFocusManager;
private ActionHandler<FilesActivity> mActions;
@@ -83,6 +86,7 @@
super.onCreate(icicle);
mClipper = DocumentsApplication.getDocumentClipper(this);
+ mSelectionMgr = new SelectionManager(SelectionManager.MODE_MULTIPLE);
mMenuManager = new MenuManager(
mSearchManager,
mState,
@@ -344,6 +348,11 @@
return mConfig;
}
+ public SelectionManager getSelectionManager(
+ DocumentsAdapter adapter, SelectionPredicate canSetState) {
+ return mSelectionMgr.reset(adapter, canSetState);
+ }
+
@Override
public FocusManager getFocusManager(RecyclerView view, Model model) {
return mFocusManager.reset(view, model);
@@ -356,7 +365,7 @@
@Override
public ActionHandler<FilesActivity> getActionHandler(
- Model model, MultiSelectManager selectionMgr, boolean searchMode) {
+ Model model, SelectionManager selectionMgr, boolean searchMode) {
// provide our friend, RootsFragment, early access to this special feature!
if (model == null || selectionMgr == null) {
diff --git a/src/com/android/documentsui/picker/ActionHandler.java b/src/com/android/documentsui/picker/ActionHandler.java
index 1117363..fbf4a43 100644
--- a/src/com/android/documentsui/picker/ActionHandler.java
+++ b/src/com/android/documentsui/picker/ActionHandler.java
@@ -28,6 +28,7 @@
import android.util.Log;
import com.android.documentsui.AbstractActionHandler;
+import com.android.documentsui.ActivityConfig;
import com.android.documentsui.DocumentsAccess;
import com.android.documentsui.Metrics;
import com.android.documentsui.base.DocumentInfo;
@@ -38,13 +39,12 @@
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.Shared;
import com.android.documentsui.base.State;
-import com.android.documentsui.ActivityConfig;
import com.android.documentsui.dirlist.DocumentDetails;
import com.android.documentsui.dirlist.Model;
import com.android.documentsui.dirlist.Model.Update;
-import com.android.documentsui.dirlist.MultiSelectManager;
import com.android.documentsui.picker.ActionHandler.Addons;
import com.android.documentsui.roots.RootsAccess;
+import com.android.documentsui.selection.SelectionManager;
import java.util.concurrent.Executor;
@@ -175,7 +175,7 @@
mConfig.modelLoadObserved = true;
}
- ActionHandler<T> reset(Model model, MultiSelectManager selectionMgr, boolean searchMode) {
+ ActionHandler<T> reset(Model model, SelectionManager selectionMgr, boolean searchMode) {
mConfig.reset(model, selectionMgr, searchMode);
return this;
}
@@ -183,7 +183,7 @@
private static final class Config {
@Nullable Model model;
- @Nullable MultiSelectManager selectionMgr;
+ @Nullable SelectionManager selectionMgr;
boolean searchMode;
// We use this to keep track of whether a model has been previously loaded or not so we can
@@ -196,7 +196,8 @@
mModelUpdateListener = modelUpdateListener;
}
- public void reset(Model model, MultiSelectManager selectionMgr, boolean searchMode) {
+
+ public void reset(Model model, SelectionManager selectionMgr, boolean searchMode) {
assert(model != null);
this.model = model;
diff --git a/src/com/android/documentsui/picker/PickActivity.java b/src/com/android/documentsui/picker/PickActivity.java
index da8b97b..2196ba9 100644
--- a/src/com/android/documentsui/picker/PickActivity.java
+++ b/src/com/android/documentsui/picker/PickActivity.java
@@ -57,9 +57,11 @@
import com.android.documentsui.base.State;
import com.android.documentsui.ActivityConfig;
import com.android.documentsui.dirlist.DirectoryFragment;
+import com.android.documentsui.dirlist.DocumentsAdapter;
import com.android.documentsui.dirlist.Model;
-import com.android.documentsui.dirlist.MultiSelectManager;
import com.android.documentsui.picker.LastAccessedProvider.Columns;
+import com.android.documentsui.selection.SelectionManager;
+import com.android.documentsui.selection.SelectionManager.SelectionPredicate;
import com.android.documentsui.services.FileOperationService;
import com.android.documentsui.sidebar.RootsFragment;
import com.android.documentsui.ui.DialogController;
@@ -73,6 +75,8 @@
private static final int CODE_FORWARD = 42;
private static final String TAG = "PickActivity";
private final Config mConfig = new Config();
+
+ private SelectionManager mSelectionMgr;
private FocusManager mFocusManager;
private MenuManager mMenuManager;
private ActionHandler<PickActivity> mActionHandler;
@@ -85,6 +89,10 @@
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
+ mSelectionMgr = new SelectionManager(
+ mState.allowMultiple
+ ? SelectionManager.MODE_MULTIPLE
+ : SelectionManager.MODE_SINGLE);
mFocusManager = new FocusManager(getColor(R.color.accent_dark));
mMenuManager = new MenuManager(mSearchManager, mState, new DirectoryDetails(this));
mActionHandler = new ActionHandler<>(
@@ -398,6 +406,11 @@
return mConfig;
}
+ public SelectionManager getSelectionManager(
+ DocumentsAdapter adapter, SelectionPredicate canSetState) {
+ return mSelectionMgr.reset(adapter, canSetState);
+ }
+
@Override
public FocusManager getFocusManager(RecyclerView view, Model model) {
return mFocusManager.reset(view, model);
@@ -410,7 +423,7 @@
@Override
public ActionHandler<PickActivity> getActionHandler(
- Model model, MultiSelectManager selectionMgr, boolean searchMode) {
+ Model model, SelectionManager selectionMgr, boolean searchMode) {
// provide our friend, RootsFragment, early access to this special feature!
if (model == null || selectionMgr == null) {
diff --git a/src/com/android/documentsui/dirlist/BandController.java b/src/com/android/documentsui/selection/BandController.java
similarity index 95%
rename from src/com/android/documentsui/dirlist/BandController.java
rename to src/com/android/documentsui/selection/BandController.java
index 429b326..a255e52 100644
--- a/src/com/android/documentsui/dirlist/BandController.java
+++ b/src/com/android/documentsui/selection/BandController.java
@@ -14,12 +14,10 @@
* limitations under the License.
*/
-package com.android.documentsui.dirlist;
+package com.android.documentsui.selection;
import static com.android.documentsui.base.Shared.DEBUG;
-import static com.android.documentsui.dirlist.ModelBackedDocumentsAdapter.ITEM_TYPE_DIRECTORY;
-import static com.android.documentsui.dirlist.ModelBackedDocumentsAdapter.ITEM_TYPE_DOCUMENT;
-import static com.android.documentsui.dirlist.ViewAutoScroller.NOT_SET;
+import static com.android.documentsui.ui.ViewAutoScroller.NOT_SET;
import android.graphics.Point;
import android.graphics.Rect;
@@ -37,9 +35,10 @@
import com.android.documentsui.R;
import com.android.documentsui.base.Events.InputEvent;
-import com.android.documentsui.dirlist.MultiSelectManager.Selection;
-import com.android.documentsui.dirlist.ViewAutoScroller.ScrollActionDelegate;
-import com.android.documentsui.dirlist.ViewAutoScroller.ScrollDistanceDelegate;
+import com.android.documentsui.dirlist.DocumentsAdapter;
+import com.android.documentsui.ui.ViewAutoScroller;
+import com.android.documentsui.ui.ViewAutoScroller.ScrollActionDelegate;
+import com.android.documentsui.ui.ViewAutoScroller.ScrollDistanceDelegate;
import java.util.ArrayList;
import java.util.Collections;
@@ -47,11 +46,12 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.function.IntPredicate;
/**
* Provides mouse driven band-select support when used in conjunction with {@link RecyclerView}
- * and {@link MultiSelectManager}. This class is responsible for rendering the band select
- * overlay and selecting overlaid items via MultiSelectManager.
+ * and {@link SelectionManager}. This class is responsible for rendering the band select
+ * overlay and selecting overlaid items via SelectionManager.
*/
public class BandController extends OnScrollListener {
@@ -60,7 +60,7 @@
private final Runnable mModelBuilder;
private final SelectionEnvironment mEnvironment;
private final DocumentsAdapter mAdapter;
- private final MultiSelectManager mSelectionManager;
+ private final SelectionManager mSelectionManager;
private final Runnable mViewScroller;
private final GridModel.OnSelectionChangedListener mGridListener;
@@ -74,14 +74,16 @@
public BandController(
final RecyclerView view,
DocumentsAdapter adapter,
- MultiSelectManager selectionManager) {
- this(new RuntimeSelectionEnvironment(view), adapter, selectionManager);
+ SelectionManager selectionManager,
+ IntPredicate gridItemTester) {
+ this(new RuntimeSelectionEnvironment(view), adapter, selectionManager, gridItemTester);
}
private BandController(
SelectionEnvironment env,
DocumentsAdapter adapter,
- MultiSelectManager selectionManager) {
+ SelectionManager selectionManager,
+ IntPredicate gridItemTester) {
selectionManager.bindContoller(this);
@@ -161,7 +163,7 @@
mModelBuilder = new Runnable() {
@Override
public void run() {
- mModel = new GridModel(mEnvironment, mAdapter);
+ mModel = new GridModel(mEnvironment, gridItemTester, mAdapter);
mModel.addOnSelectionChangedListener(mGridListener);
}
};
@@ -175,7 +177,7 @@
mSelection = selection;
}
- boolean onInterceptTouchEvent(InputEvent e) {
+ public boolean onInterceptTouchEvent(InputEvent e) {
if (shouldStart(e)) {
if (!e.isCtrlKeyDown()) {
mSelectionManager.clearSelection();
@@ -235,7 +237,7 @@
* Processes a MotionEvent by starting, ending, or resizing the band select overlay.
* @param input
*/
- void onTouchEvent(InputEvent input) {
+ public void onTouchEvent(InputEvent input) {
assert(input.isMouseEvent());
if (shouldStop(input)) {
@@ -359,6 +361,7 @@
private static final int LOWER_RIGHT = LOWER | RIGHT;
private final SelectionEnvironment mHelper;
+ private final IntPredicate mGridItemTester;
private final DocumentsAdapter mAdapter;
private final List<GridModel.OnSelectionChangedListener> mOnSelectionChangedListeners =
@@ -398,9 +401,10 @@
// should expand from when Shift+click is used.
private int mPositionNearestOrigin = NOT_SET;
- GridModel(SelectionEnvironment helper, DocumentsAdapter adapter) {
+ GridModel(SelectionEnvironment helper, IntPredicate gridItemTester, DocumentsAdapter adapter) {
mHelper = helper;
mAdapter = adapter;
+ mGridItemTester = gridItemTester;
mHelper.addOnScrollListener(this);
}
@@ -485,7 +489,7 @@
// synchronously, while views are attached asynchronously. As a result items which
// are in the adapter may not actually have a corresponding view (yet).
if (mHelper.hasView(adapterPosition) &&
- !mHelper.isLayoutItem(adapterPosition) &&
+ mGridItemTester.test(adapterPosition) &&
!mKnownPositions.get(adapterPosition)) {
mKnownPositions.put(adapterPosition, true);
recordItemData(mHelper.getAbsoluteRectForChildViewAt(i), adapterPosition);
@@ -1008,10 +1012,6 @@
int getChildCount();
int getVisibleChildCount();
/**
- * Layout items are excluded from the GridModel.
- */
- boolean isLayoutItem(int adapterPosition);
- /**
* Items may be in the adapter, but without an attached view.
*/
boolean hasView(int adapterPosition);
@@ -1124,22 +1124,8 @@
}
@Override
- public boolean isLayoutItem(int pos) {
- // The band selection model only operates on documents and directories. Exclude other
- // types of adapter items (e.g. whitespace items like dividers).
- RecyclerView.ViewHolder vh = mView.findViewHolderForAdapterPosition(pos);
- switch (vh.getItemViewType()) {
- case ITEM_TYPE_DOCUMENT:
- case ITEM_TYPE_DIRECTORY:
- return false;
- default:
- return true;
- }
- }
-
- @Override
public boolean hasView(int pos) {
return mView.findViewHolderForAdapterPosition(pos) != null;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/com/android/documentsui/dirlist/GestureSelector.java b/src/com/android/documentsui/selection/GestureSelector.java
similarity index 93%
rename from src/com/android/documentsui/dirlist/GestureSelector.java
rename to src/com/android/documentsui/selection/GestureSelector.java
index d0b68ae..08afac5 100644
--- a/src/com/android/documentsui/dirlist/GestureSelector.java
+++ b/src/com/android/documentsui/selection/GestureSelector.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.documentsui.dirlist;
+package com.android.documentsui.selection;
import android.graphics.Point;
import android.support.annotation.VisibleForTesting;
@@ -22,8 +22,9 @@
import android.view.View;
import com.android.documentsui.base.Events.InputEvent;
-import com.android.documentsui.dirlist.ViewAutoScroller.ScrollActionDelegate;
-import com.android.documentsui.dirlist.ViewAutoScroller.ScrollDistanceDelegate;
+import com.android.documentsui.ui.ViewAutoScroller;
+import com.android.documentsui.ui.ViewAutoScroller.ScrollActionDelegate;
+import com.android.documentsui.ui.ViewAutoScroller.ScrollDistanceDelegate;
import java.util.function.IntSupplier;
@@ -33,9 +34,9 @@
* Helper class used to intercept events that could cause a gesture multi-select, and keeps
* the interception going if necessary.
*/
-final class GestureSelector {
+public final class GestureSelector {
- private final MultiSelectManager mSelectionMgr;
+ private final SelectionManager mSelectionMgr;
private final Runnable mDragScroller;
private final IntSupplier mHeight;
private final ViewFinder mViewFinder;
@@ -44,7 +45,7 @@
private Point mLastInterceptedPoint;
GestureSelector(
- MultiSelectManager selectionMgr,
+ SelectionManager selectionMgr,
IntSupplier heightSupplier,
ViewFinder viewFinder,
ScrollActionDelegate actionDelegate) {
@@ -72,8 +73,8 @@
mDragScroller = new ViewAutoScroller(distanceDelegate, actionDelegate);
}
- static GestureSelector create(
- MultiSelectManager selectionMgr,
+ public static GestureSelector create(
+ SelectionManager selectionMgr,
RecyclerView scrollView) {
ScrollActionDelegate actionDelegate = new ScrollActionDelegate() {
@Override
@@ -102,7 +103,7 @@
}
// Explicitly kick off a gesture multi-select.
- boolean start(InputEvent event) {
+ public boolean start(InputEvent event) {
if (mStarted) {
return false;
}
diff --git a/src/com/android/documentsui/selection/Range.java b/src/com/android/documentsui/selection/Range.java
new file mode 100644
index 0000000..cd04f90
--- /dev/null
+++ b/src/com/android/documentsui/selection/Range.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2016 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.selection;
+
+import static com.android.documentsui.base.Shared.DEBUG;
+
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+
+import com.android.documentsui.selection.SelectionManager.RangeType;
+
+/**
+ * Class providing support for managing range selections.
+ */
+final class Range {
+ private static final int UNDEFINED = -1;
+
+ private final Range.RangeUpdater mUpdater;
+ private final int mBegin;
+ private int mEnd = UNDEFINED;
+
+ public Range(Range.RangeUpdater updater, int begin) {
+ if (DEBUG) Log.d(SelectionManager.TAG, "New Ranger created beginning @ " + begin);
+ mUpdater = updater;
+ mBegin = begin;
+ }
+
+ void snapSelection(int position, @RangeType int type) {
+ assert(position != RecyclerView.NO_POSITION);
+
+ if (mEnd == UNDEFINED || mEnd == mBegin) {
+ // Reset mEnd so it can be established in establishRange.
+ mEnd = UNDEFINED;
+ establishRange(position, type);
+ } else {
+ reviseRange(position, type);
+ }
+ }
+
+ private void establishRange(int position, @RangeType int type) {
+ assert(mEnd == UNDEFINED);
+
+ if (position == mBegin) {
+ mEnd = position;
+ }
+
+ if (position > mBegin) {
+ updateRange(mBegin + 1, position, true, type);
+ } else if (position < mBegin) {
+ updateRange(position, mBegin - 1, true, type);
+ }
+
+ mEnd = position;
+ }
+
+ private void reviseRange(int position, @RangeType int type) {
+ assert(mEnd != UNDEFINED);
+ assert(mBegin != mEnd);
+
+ if (position == mEnd) {
+ if (DEBUG) Log.v(SelectionManager.TAG, "Ignoring no-op revision for range: " + this);
+ }
+
+ if (mEnd > mBegin) {
+ reviseAscendingRange(position, type);
+ } else if (mEnd < mBegin) {
+ reviseDescendingRange(position, type);
+ }
+ // the "else" case is covered by checkState at beginning of method.
+
+ mEnd = position;
+ }
+
+ /**
+ * Updates an existing ascending seleciton.
+ * @param position
+ */
+ private void reviseAscendingRange(int position, @RangeType int type) {
+ // Reducing or reversing the range....
+ if (position < mEnd) {
+ if (position < mBegin) {
+ updateRange(mBegin + 1, mEnd, false, type);
+ updateRange(position, mBegin -1, true, type);
+ } else {
+ updateRange(position + 1, mEnd, false, type);
+ }
+ }
+
+ // Extending the range...
+ else if (position > mEnd) {
+ updateRange(mEnd + 1, position, true, type);
+ }
+ }
+
+ private void reviseDescendingRange(int position, @RangeType int type) {
+ // Reducing or reversing the range....
+ if (position > mEnd) {
+ if (position > mBegin) {
+ updateRange(mEnd, mBegin - 1, false, type);
+ updateRange(mBegin + 1, position, true, type);
+ } else {
+ updateRange(mEnd, position - 1, false, type);
+ }
+ }
+
+ // Extending the range...
+ else if (position < mEnd) {
+ updateRange(position, mEnd - 1, true, type);
+ }
+ }
+
+ /**
+ * Try to set selection state for all elements in range. Not that callbacks can cancel
+ * selection of specific items, so some or even all items may not reflect the desired state
+ * after the update is complete.
+ *
+ * @param begin Adapter position for range start (inclusive).
+ * @param end Adapter position for range end (inclusive).
+ * @param selected New selection state.
+ */
+ private void updateRange(int begin, int end, boolean selected, @RangeType int type) {
+ mUpdater.updateForRange(begin, end, selected, type);
+ }
+
+ @Override
+ public String toString() {
+ return "Range{begin=" + mBegin + ", end=" + mEnd + "}";
+ }
+
+ /*
+ * @see {@link MultiSelectManager#updateForRegularRange(int, int , boolean)} and {@link
+ * MultiSelectManager#updateForProvisionalRange(int, int, boolean)}
+ */
+ @FunctionalInterface
+ interface RangeUpdater {
+ void updateForRange(int begin, int end, boolean selected, @RangeType int type);
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/documentsui/selection/Selection.java b/src/com/android/documentsui/selection/Selection.java
new file mode 100644
index 0000000..8775c58
--- /dev/null
+++ b/src/com/android/documentsui/selection/Selection.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2016 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.selection;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * Object representing the current selection. Provides read only access
+ * public access, and private write access.
+ */
+public final class Selection implements Iterable<String>, Parcelable {
+
+ // 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
+ // completing a band select operation. Provisional selections are selections which have been
+ // temporarily created by an in-progress band select operation (once the user releases the
+ // mouse button during a band select operation, the selected items become saved). The total
+ // selection is the combination of both the saved selection and the provisional
+ // selection. Tracking both separately is necessary to ensure that saved selections do not
+ // become deselected when they are removed from the provisional selection; for example, if
+ // 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
+ // selection must be tracked separately.
+ final Set<String> mSelection;
+ final Set<String> mProvisionalSelection;
+
+ public Selection() {
+ mSelection = new HashSet<>();
+ mProvisionalSelection = new HashSet<>();
+ }
+
+ /**
+ * Used by CREATOR.
+ */
+ private Selection(Set<String> selection) {
+ mSelection = selection;
+ mProvisionalSelection = new HashSet<>();
+ }
+
+ /**
+ * @param id
+ * @return true if the position is currently selected.
+ */
+ public boolean contains(@Nullable String id) {
+ return mSelection.contains(id) || mProvisionalSelection.contains(id);
+ }
+
+ /**
+ * Returns an {@link Iterator} that iterators over the selection, *excluding*
+ * any provisional selection.
+ *
+ * {@inheritDoc}
+ */
+ @Override
+ public Iterator<String> iterator() {
+ return mSelection.iterator();
+ }
+
+ /**
+ * @return size of the selection including both final and provisional selected items.
+ */
+ public int size() {
+ return mSelection.size() + mProvisionalSelection.size();
+ }
+
+ /**
+ * @return true if the selection is empty.
+ */
+ public boolean isEmpty() {
+ return mSelection.isEmpty() && mProvisionalSelection.isEmpty();
+ }
+
+ /**
+ * 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
+ * one (if it exists) is abandoned.
+ * @return Map of ids added or removed. Added ids have a value of true, removed are false.
+ */
+ @VisibleForTesting
+ protected Map<String, Boolean> setProvisionalSelection(Set<String> newSelection) {
+ Map<String, Boolean> delta = new HashMap<>();
+
+ 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
+ // provisional selection.
+ if (!newSelection.contains(id)) {
+ delta.put(id, false);
+ }
+ }
+
+ for (String id: newSelection) {
+ // Mark each item that was not previously in the selection but is in the new
+ // provisional selection.
+ if (!mSelection.contains(id) && !mProvisionalSelection.contains(id)) {
+ delta.put(id, true);
+ }
+ }
+
+ // Now, iterate through the changes and actually add/remove them to/from the current
+ // selection. This could not be done in the previous loops because changing the size of
+ // the selection mid-iteration changes iteration order erroneously.
+ for (Map.Entry<String, Boolean> entry: delta.entrySet()) {
+ String id = entry.getKey();
+ if (entry.getValue()) {
+ mProvisionalSelection.add(id);
+ } else {
+ mProvisionalSelection.remove(id);
+ }
+ }
+
+ return delta;
+ }
+
+ /**
+ * Saves the existing provisional selection. Once the provisional selection is saved,
+ * subsequent provisional selections which are different from this existing one cannot
+ * cause items in this existing provisional selection to become deselected.
+ */
+ @VisibleForTesting
+ protected void applyProvisionalSelection() {
+ mSelection.addAll(mProvisionalSelection);
+ mProvisionalSelection.clear();
+ }
+
+ /**
+ * Abandons the existing provisional selection so that all items provisionally selected are
+ * now deselected.
+ */
+ @VisibleForTesting
+ void cancelProvisionalSelection() {
+ mProvisionalSelection.clear();
+ }
+
+ /** @hide */
+ @VisibleForTesting
+ public boolean add(String id) {
+ if (!mSelection.contains(id)) {
+ mSelection.add(id);
+ return true;
+ }
+ return false;
+ }
+
+ /** @hide */
+ @VisibleForTesting
+ boolean remove(String id) {
+ if (mSelection.contains(id)) {
+ mSelection.remove(id);
+ return true;
+ }
+ return false;
+ }
+
+ public void clear() {
+ mSelection.clear();
+ }
+
+ /**
+ * Trims this selection to be the intersection of itself with the set of given IDs.
+ */
+ public void intersect(Collection<String> ids) {
+ mSelection.retainAll(ids);
+ mProvisionalSelection.retainAll(ids);
+ }
+
+ @VisibleForTesting
+ void copyFrom(Selection source) {
+ mSelection.clear();
+ mSelection.addAll(source.mSelection);
+
+ mProvisionalSelection.clear();
+ mProvisionalSelection.addAll(source.mProvisionalSelection);
+ }
+
+ @Override
+ public String toString() {
+ if (size() <= 0) {
+ return "size=0, items=[]";
+ }
+
+ StringBuilder buffer = new StringBuilder(size() * 28);
+ buffer.append("Selection{")
+ .append("applied{size=" + mSelection.size())
+ .append(", entries=" + mSelection)
+ .append("}, provisional{size=" + mProvisionalSelection.size())
+ .append(", entries=" + mProvisionalSelection)
+ .append("}}");
+ return buffer.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return mSelection.hashCode() ^ mProvisionalSelection.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object that) {
+ if (this == that) {
+ return true;
+ }
+
+ if (!(that instanceof Selection)) {
+ return false;
+ }
+
+ return mSelection.equals(((Selection) that).mSelection) &&
+ mProvisionalSelection.equals(((Selection) that).mProvisionalSelection);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeStringList(new ArrayList<>(mSelection));
+ // We don't include provisional selection since it is
+ // typically coupled to some other runtime state (like a band).
+ }
+
+ public static final ClassLoaderCreator<Selection> CREATOR =
+ new ClassLoaderCreator<Selection>() {
+ @Override
+ public Selection createFromParcel(Parcel in) {
+ return createFromParcel(in, null);
+ }
+
+ @Override
+ public Selection createFromParcel(Parcel in, ClassLoader loader) {
+ ArrayList<String> selected = new ArrayList<>();
+ in.readStringList(selected);
+
+ return new Selection(new HashSet<>(selected));
+ }
+
+ @Override
+ public Selection[] newArray(int size) {
+ return new Selection[size];
+ }
+ };
+}
\ No newline at end of file
diff --git a/src/com/android/documentsui/selection/SelectionManager.java b/src/com/android/documentsui/selection/SelectionManager.java
new file mode 100644
index 0000000..98d1cda
--- /dev/null
+++ b/src/com/android/documentsui/selection/SelectionManager.java
@@ -0,0 +1,517 @@
+/*
+ * 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.selection;
+
+import static com.android.documentsui.base.Shared.DEBUG;
+
+import android.annotation.IntDef;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+
+import com.android.documentsui.dirlist.DocumentsAdapter;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+/**
+ * MultiSelectManager provides support traditional multi-item selection support to RecyclerView.
+ * Additionally it can be configured to restrict selection to a single element, @see
+ * #setSelectMode.
+ */
+public final class SelectionManager {
+
+ @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_SINGLE = 1;
+
+ @IntDef({
+ RANGE_REGULAR,
+ RANGE_PROVISIONAL
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RangeType {}
+ public static final int RANGE_REGULAR = 0;
+ public static final int RANGE_PROVISIONAL = 1;
+
+ static final String TAG = "SelectionManager";
+
+ private final Selection mSelection = new Selection();
+
+ private final List<Callback> mCallbacks = new ArrayList<>(1);
+ private final List<ItemCallback> mItemCallbacks = new ArrayList<>(1);
+
+ private @Nullable DocumentsAdapter mAdapter;
+ private @Nullable Range mRanger;
+ private boolean mSingleSelect;
+
+ private RecyclerView.AdapterDataObserver mAdapterObserver;
+ private SelectionPredicate mCanSetState;
+
+ public SelectionManager(@SelectionMode int mode) {
+ mSingleSelect = mode == MODE_SINGLE;
+ }
+
+ public SelectionManager reset(DocumentsAdapter adapter, SelectionPredicate canSetState) {
+
+ mCallbacks.clear();
+ mItemCallbacks.clear();
+ if (mAdapter != null && mAdapterObserver != null) {
+ mAdapter.unregisterAdapterDataObserver(mAdapterObserver);
+ }
+
+ clearSelectionQuietly();
+
+ assert(adapter != null);
+ assert(canSetState != null);
+
+ mAdapter = adapter;
+ mCanSetState = canSetState;
+
+ mAdapterObserver = new RecyclerView.AdapterDataObserver() {
+
+ private List<String> mModelIds;
+
+ @Override
+ public void onChanged() {
+ mModelIds = mAdapter.getModelIds();
+
+ // Update the selection to remove any disappeared IDs.
+ mSelection.cancelProvisionalSelection();
+ mSelection.intersect(mModelIds);
+ }
+
+ @Override
+ public void onItemRangeChanged(
+ int startPosition, int itemCount, Object payload) {
+ // No change in position. Ignoring.
+ }
+
+ @Override
+ public void onItemRangeInserted(int startPosition, int itemCount) {
+ mSelection.cancelProvisionalSelection();
+ }
+
+ @Override
+ public void onItemRangeRemoved(int startPosition, int itemCount) {
+ assert(startPosition >= 0);
+ assert(itemCount > 0);
+
+ mSelection.cancelProvisionalSelection();
+ // Remove any disappeared IDs from the selection.
+ mSelection.intersect(mModelIds);
+ }
+
+ @Override
+ public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+ throw new UnsupportedOperationException();
+ }
+ };
+
+ mAdapter.registerAdapterDataObserver(mAdapterObserver);
+ return this;
+ }
+
+ void bindContoller(BandController controller) {
+ // Provides BandController with access to private mSelection state.
+ controller.bindSelection(mSelection);
+ }
+
+ /**
+ * Adds {@code callback} such that it will be notified when {@code MultiSelectManager}
+ * events occur.
+ *
+ * @param callback
+ */
+ public void addCallback(Callback callback) {
+ assert(callback != null);
+ mCallbacks.add(callback);
+ }
+
+ public void addItemCallback(ItemCallback itemCallback) {
+ assert(itemCallback != null);
+ mItemCallbacks.add(itemCallback);
+ }
+
+ public boolean hasSelection() {
+ return !mSelection.isEmpty();
+ }
+
+ /**
+ * Returns a Selection object that provides a live view
+ * on the current selection.
+ *
+ * @see #getSelection(Selection) on how to get a snapshot
+ * of the selection that will not reflect future changes
+ * to selection.
+ *
+ * @return The current selection.
+ */
+ public Selection getSelection() {
+ return mSelection;
+ }
+
+ /**
+ * Updates {@code dest} to reflect the current selection.
+ * @param dest
+ *
+ * @return The Selection instance passed in, for convenience.
+ */
+ public Selection getSelection(Selection dest) {
+ dest.copyFrom(mSelection);
+ return dest;
+ }
+
+ public void replaceSelection(Iterable<String> ids) {
+ clearSelection();
+ setItemsSelected(ids, true);
+ }
+
+ /**
+ * Restores the selected state of specified items. Used in cases such as restore the selection
+ * after rotation etc.
+ */
+ public void restoreSelection(Selection other) {
+ setItemsSelectedQuietly(other.mSelection, true);
+ // NOTE: We intentionally don't restore provisional selection. It's provisional.
+ notifySelectionRestored();
+ }
+
+ /**
+ * Sets the selected state of the specified items. Note that the callback will NOT
+ * be consulted to see if an item can be selected.
+ *
+ * @param ids
+ * @param selected
+ * @return
+ */
+ public boolean setItemsSelected(Iterable<String> ids, boolean selected) {
+ final boolean changed = setItemsSelectedQuietly(ids, selected);
+ notifySelectionChanged();
+ return changed;
+ }
+
+ private boolean setItemsSelectedQuietly(Iterable<String> ids, boolean selected) {
+ boolean changed = false;
+ for (String id: ids) {
+ final boolean itemChanged =
+ selected
+ ? canSetState(id, true) && mSelection.add(id)
+ : canSetState(id, false) && mSelection.remove(id);
+ if (itemChanged) {
+ notifyItemStateChanged(id, selected);
+ }
+ changed |= itemChanged;
+ }
+ return changed;
+ }
+
+ /**
+ * Clears the selection and notifies (if something changes).
+ */
+ public void clearSelection() {
+ if (!hasSelection()) {
+ return;
+ }
+
+ clearSelectionQuietly();
+ notifySelectionChanged();
+ }
+
+ /**
+ * Clears the selection, without notifying selection listeners. UI elements still need to be
+ * notified about state changes so that they can update their appearance.
+ */
+ private void clearSelectionQuietly() {
+ mRanger = null;
+
+ if (!hasSelection()) {
+ return;
+ }
+
+ Selection oldSelection = getSelection(new Selection());
+ mSelection.clear();
+
+ for (String id: oldSelection.mSelection) {
+ notifyItemStateChanged(id, false);
+ }
+ for (String id: oldSelection.mProvisionalSelection) {
+ notifyItemStateChanged(id, false);
+ }
+ }
+
+ /**
+ * Toggles selection on the item with the given model ID.
+ *
+ * @param modelId
+ */
+ public void toggleSelection(String modelId) {
+ assert(modelId != null);
+
+ final boolean changed = mSelection.contains(modelId)
+ ? attemptDeselect(modelId)
+ : attemptSelect(modelId);
+
+ if (changed) {
+ notifySelectionChanged();
+ }
+ }
+
+ /**
+ * Starts a range selection. If a range selection is already active, this will start a new range
+ * selection (which will reset the range anchor).
+ *
+ * @param pos The anchor position for the selection range.
+ */
+ public void startRangeSelection(int pos) {
+ attemptSelect(mAdapter.getModelId(pos));
+ setSelectionRangeBegin(pos);
+ }
+
+ public void snapRangeSelection(int pos) {
+ snapRangeSelection(pos, RANGE_REGULAR);
+ }
+
+ void snapProvisionalRangeSelection(int pos) {
+ snapRangeSelection(pos, RANGE_PROVISIONAL);
+ }
+
+ /**
+ * Sets the end point for the current range selection, started by a call to
+ * {@link #startRangeSelection(int)}. This function should only be called when a range selection
+ * is active (see {@link #isRangeSelectionActive()}. Items in the range [anchor, end] will be
+ * selected or in provisional select, depending on the type supplied. Note that if the type is
+ * provisional select, one should do {@link Selection#applyProvisionalSelection()} at some point
+ * before calling on {@link #endRangeSelection()}.
+ *
+ * @param pos The new end position for the selection range.
+ * @param type The type of selection the range should utilize.
+ */
+ private void snapRangeSelection(int pos, @RangeType int type) {
+ if (!isRangeSelectionActive()) {
+ throw new IllegalStateException("Range start point not set.");
+ }
+
+ mRanger.snapSelection(pos, type);
+
+ // We're being lazy here notifying even when something might not have changed.
+ // To make this more correct, we'd need to update the Ranger class to return
+ // information about what has changed.
+ notifySelectionChanged();
+ }
+
+ /**
+ * Stops an in-progress range selection. All selection done with
+ * {@link #snapRangeSelection(int, int)} with type RANGE_PROVISIONAL will be lost if
+ * {@link Selection#applyProvisionalSelection()} is not called beforehand.
+ */
+ public void endRangeSelection() {
+ mRanger = null;
+ // Clean up in case there was any leftover provisional selection
+ mSelection.cancelProvisionalSelection();
+ }
+
+ /**
+ * @return Whether or not there is a current range selection active.
+ */
+ public boolean isRangeSelectionActive() {
+ return mRanger != null;
+ }
+
+ /**
+ * Sets the magic location at which a selection range begins (the selection anchor). This value
+ * is consulted when determining how to extend, and modify selection ranges. Calling this when a
+ * range selection is active will reset the range selection.
+ */
+ public void setSelectionRangeBegin(int position) {
+ if (position == RecyclerView.NO_POSITION) {
+ return;
+ }
+
+ if (mSelection.contains(mAdapter.getModelId(position))) {
+ mRanger = new Range(this::updateForRange, position);
+ }
+ }
+
+ /**
+ * @param modelId
+ * @return True if the update was applied.
+ */
+ private boolean selectAndNotify(String modelId) {
+ boolean changed = mSelection.add(modelId);
+ if (changed) {
+ notifyItemStateChanged(modelId, true);
+ }
+ return changed;
+ }
+
+ /**
+ * @param id
+ * @return True if the update was applied.
+ */
+ private boolean attemptDeselect(String id) {
+ assert(id != null);
+ if (canSetState(id, false)) {
+ mSelection.remove(id);
+ notifyItemStateChanged(id, false);
+ if (DEBUG) Log.d(TAG, "Selection after deselect: " + mSelection);
+ return true;
+ } else {
+ if (DEBUG) Log.d(TAG, "Select cancelled by listener.");
+ return false;
+ }
+ }
+
+ /**
+ * @param id
+ * @return True if the update was applied.
+ */
+ private boolean attemptSelect(String id) {
+ assert(id != null);
+ boolean canSelect = canSetState(id, true);
+ if (!canSelect) {
+ return false;
+ }
+ if (mSingleSelect && hasSelection()) {
+ clearSelectionQuietly();
+ }
+
+ selectAndNotify(id);
+ return true;
+ }
+
+ boolean canSetState(String id, boolean nextState) {
+ return mCanSetState.test(id, nextState);
+ }
+
+ /**
+ * Notifies registered listeners when the selection status of a single item
+ * (identified by {@code position}) changes.
+ */
+ void notifyItemStateChanged(String id, boolean selected) {
+ assert(id != null);
+ int lastListener = mItemCallbacks.size() - 1;
+ for (int i = lastListener; i >= 0; i--) {
+ mItemCallbacks.get(i).onItemStateChanged(id, selected);
+ }
+ mAdapter.onItemSelectionChanged(id);
+ }
+
+ /**
+ * Notifies registered listeners when the selection has changed. This
+ * notification should be sent only once a full series of changes
+ * is complete, e.g. clearingSelection, or updating the single
+ * selection from one item to another.
+ */
+ void notifySelectionChanged() {
+ int lastListener = mCallbacks.size() - 1;
+ for (int i = lastListener; i > -1; i--) {
+ mCallbacks.get(i).onSelectionChanged();
+ }
+ }
+
+ private void notifySelectionRestored() {
+ int lastListener = mCallbacks.size() - 1;
+ for (int i = lastListener; i > -1; i--) {
+ mCallbacks.get(i).onSelectionRestored();
+ }
+ }
+
+ void updateForRange(int begin, int end, boolean selected, @RangeType int type) {
+ switch (type) {
+ case RANGE_REGULAR:
+ updateForRegularRange(begin, end, selected);
+ break;
+ case RANGE_PROVISIONAL:
+ updateForProvisionalRange(begin, end, selected);
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid range type: " + type);
+ }
+ }
+
+ private void updateForRegularRange(int begin, int end, boolean selected) {
+ assert(end >= begin);
+ for (int i = begin; i <= end; i++) {
+ String id = mAdapter.getModelId(i);
+ if (id == null) {
+ continue;
+ }
+
+ if (selected) {
+ boolean canSelect = canSetState(id, true);
+ if (canSelect) {
+ if (mSingleSelect && hasSelection()) {
+ clearSelectionQuietly();
+ }
+ selectAndNotify(id);
+ }
+ } else {
+ attemptDeselect(id);
+ }
+ }
+ }
+
+ private void updateForProvisionalRange(int begin, int end, boolean selected) {
+ assert (end >= begin);
+ for (int i = begin; i <= end; i++) {
+ String id = mAdapter.getModelId(i);
+ if (id == null) {
+ continue;
+ }
+ if (selected) {
+ boolean canSelect = canSetState(id, true);
+ if (canSelect) {
+ mSelection.mProvisionalSelection.add(id);
+ }
+ } else {
+ mSelection.mProvisionalSelection.remove(id);
+ }
+ notifyItemStateChanged(id, selected);
+ }
+ notifySelectionChanged();
+ }
+
+ public interface ItemCallback {
+ void onItemStateChanged(String id, boolean selected);
+ }
+
+ public interface Callback {
+ /**
+ * Called immediately after completion of any set of changes.
+ */
+ void onSelectionChanged();
+
+ /**
+ * Called immediately after selection is restored.
+ */
+ void onSelectionRestored();
+ }
+
+ @FunctionalInterface
+ public interface SelectionPredicate {
+ boolean test(String id, boolean nextState);
+ }
+}
diff --git a/src/com/android/documentsui/dirlist/SelectionMetadata.java b/src/com/android/documentsui/selection/SelectionMetadata.java
similarity index 93%
rename from src/com/android/documentsui/dirlist/SelectionMetadata.java
rename to src/com/android/documentsui/selection/SelectionMetadata.java
index 4c5a321..e5f64d9 100644
--- a/src/com/android/documentsui/dirlist/SelectionMetadata.java
+++ b/src/com/android/documentsui/selection/SelectionMetadata.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.documentsui.dirlist;
+package com.android.documentsui.selection;
import static com.android.documentsui.base.DocumentInfo.getCursorInt;
import static com.android.documentsui.base.DocumentInfo.getCursorString;
@@ -31,7 +31,8 @@
/**
* A class that holds metadata
*/
-class SelectionMetadata implements MenuManager.SelectionDetails, MultiSelectManager.ItemCallback {
+public class SelectionMetadata
+ implements MenuManager.SelectionDetails, SelectionManager.ItemCallback {
private static final String TAG = "SelectionMetadata";
@@ -46,7 +47,7 @@
private int mNoDeleteCount = 0;
private int mNoRenameCount = 0;
- SelectionMetadata(Function<String, Cursor> docFinder) {
+ public SelectionMetadata(Function<String, Cursor> docFinder) {
mDocFinder = docFinder;
}
diff --git a/src/com/android/documentsui/dirlist/ViewAutoScroller.java b/src/com/android/documentsui/ui/ViewAutoScroller.java
similarity index 95%
rename from src/com/android/documentsui/dirlist/ViewAutoScroller.java
rename to src/com/android/documentsui/ui/ViewAutoScroller.java
index b6e5f00..2add5ae 100644
--- a/src/com/android/documentsui/dirlist/ViewAutoScroller.java
+++ b/src/com/android/documentsui/ui/ViewAutoScroller.java
@@ -15,14 +15,14 @@
*/
-package com.android.documentsui.dirlist;
+package com.android.documentsui.ui;
import android.graphics.Point;
/**
* Provides auto-scrolling upon request when user's interaction with the application
- * introduces a natural intent to scroll. Used by {@link BandController}, {@link GestureSelector}
- * and {@link DragHoverListener} to allow auto scrolling when user either does band selection,
+ * introduces a natural intent to scroll. Used by BandController, GestureSelector,
+ * and DragHoverListener to allow auto scrolling when user either does band selection,
* attempting to drag and drop files to somewhere off the current screen, or trying to motion select
* past top/bottom of the screen.
*/
@@ -131,7 +131,7 @@
* Used by {@link run} to properly calculate the proper amount of pixels to scroll given time
* passed since scroll started, and to properly scroll / proper listener clean up if necessary.
*/
- interface ScrollDistanceDelegate {
+ public interface ScrollDistanceDelegate {
public Point getCurrentPosition();
public int getViewHeight();
public boolean isActive();
@@ -140,7 +140,7 @@
/**
* Used by {@link run} to do UI tasks, such as scrolling and rerunning at next UI cycle.
*/
- interface ScrollActionDelegate {
+ public interface ScrollActionDelegate {
public void scrollBy(int dy);
public void runAtNextFrame(Runnable r);
public void removeCallback(Runnable r);