Merge "Prioritize delete jobs by adding them to a separate thread pool." into nyc-andromeda-dev
diff --git a/src/com/android/documentsui/FilesActivity.java b/src/com/android/documentsui/FilesActivity.java
index 8445cf7..e5de6ba 100644
--- a/src/com/android/documentsui/FilesActivity.java
+++ b/src/com/android/documentsui/FilesActivity.java
@@ -86,17 +86,26 @@
if (mState.restored) {
if (DEBUG) Log.d(TAG, "Stack already resolved for uri: " + intent.getData());
} else if (!mState.stack.isEmpty()) {
- // If a non-empty stack is present in our state it was read (presumably)
+ // If a non-empty stack is present in our state, it was read (presumably)
// from EXTRA_STACK intent extra. In this case, we'll skip other means of
- // loading or restoring the stack.
+ // loading or restoring the stack (like URI).
//
- // When restoring from a stack, if a URI is present, it should only ever
- // be a launch URI, or a fake Uri from notifications.
- // Launch URIs support sensible activity management, but don't specify a real
- // content target.
- if (DEBUG) Log.d(TAG, "Launching with non-empty stack.");
- assert(uri == null || uri.getAuthority() == null ||
- LauncherActivity.isLaunchUri(uri));
+ // When restoring from a stack, if a URI is present, it should only ever be:
+ // -- a launch URI: Launch URIs support sensible activity management,
+ // but don't specify a real content target)
+ // -- a fake Uri from notifications. These URIs have no authority (TODO: details).
+ //
+ // Any other URI is *sorta* unexpected...except when browsing an archive
+ // in downloads.
+ if(uri != null
+ && uri.getAuthority() != null
+ && !uri.equals(mState.stack.peek())
+ && !LauncherActivity.isLaunchUri(uri)) {
+ if (DEBUG) Log.w(TAG,
+ "Launching with non-empty stack. Ignoring unexpected uri: " + uri);
+ } else {
+ if (DEBUG) Log.d(TAG, "Launching with non-empty stack.");
+ }
refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
} else if (Intent.ACTION_VIEW.equals(intent.getAction())) {
assert(uri != null);
diff --git a/src/com/android/documentsui/dirlist/BandController.java b/src/com/android/documentsui/dirlist/BandController.java
new file mode 100644
index 0000000..f3dc686
--- /dev/null
+++ b/src/com/android/documentsui/dirlist/BandController.java
@@ -0,0 +1,1255 @@
+/*
+ * 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.Shared.DEBUG;
+import static com.android.documentsui.dirlist.ModelBackedDocumentsAdapter.ITEM_TYPE_DIRECTORY;
+import static com.android.documentsui.dirlist.ModelBackedDocumentsAdapter.ITEM_TYPE_DOCUMENT;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import android.util.SparseIntArray;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.documentsui.Events;
+import com.android.documentsui.Events.InputEvent;
+import com.android.documentsui.Events.MotionInputEvent;
+import com.android.documentsui.R;
+import com.android.documentsui.dirlist.MultiSelectManager.Selection;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 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.
+ */
+public class BandController extends RecyclerView.OnScrollListener {
+
+ private static final int NOT_SET = -1;
+
+ private static final String TAG = "BandController";
+
+ private final Runnable mModelBuilder;
+ private final SelectionEnvironment mEnvironment;
+ private final DocumentsAdapter mAdapter;
+ private final MultiSelectManager mSelectionManager;
+ private final Runnable mViewScroller = new ViewScroller();
+ private final GridModel.OnSelectionChangedListener mGridListener;
+
+ @Nullable private Rect mBounds;
+ @Nullable private Point mCurrentPosition;
+ @Nullable private Point mOrigin;
+ @Nullable private BandController.GridModel mModel;
+
+ // The time at which the current band selection-induced scroll began. If no scroll is in
+ // progress, the value is NOT_SET.
+ private long mScrollStartTime = NOT_SET;
+ private Selection mSelection;
+
+ public BandController(
+ final RecyclerView view,
+ DocumentsAdapter adapter,
+ MultiSelectManager selectionManager) {
+ this(new RuntimeSelectionEnvironment(view), adapter, selectionManager);
+
+ view.addOnItemTouchListener(
+ new RecyclerView.OnItemTouchListener() {
+ @Override
+ public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
+ return handleEvent(new MotionInputEvent(e, view));
+ }
+ @Override
+ public void onTouchEvent(RecyclerView rv, MotionEvent e) {
+ if (Events.isMouseEvent(e)) {
+ processInputEvent(new MotionInputEvent(e, view));
+ }
+ }
+ @Override
+ public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
+ });
+ }
+
+ private BandController(
+ SelectionEnvironment env,
+ DocumentsAdapter adapter,
+ MultiSelectManager selectionManager) {
+
+ selectionManager.bindContoller(this);
+
+ mEnvironment = env;
+ mAdapter = adapter;
+ mSelectionManager = selectionManager;
+
+ mEnvironment.addOnScrollListener(this);
+
+ mAdapter.registerAdapterDataObserver(
+ new RecyclerView.AdapterDataObserver() {
+ @Override
+ public void onChanged() {
+ if (isActive()) {
+ endBandSelect();
+ }
+ }
+
+ @Override
+ public void onItemRangeChanged(
+ int startPosition, int itemCount, Object payload) {
+ // No change in position. Ignoring.
+ }
+
+ @Override
+ public void onItemRangeInserted(int startPosition, int itemCount) {
+ if (isActive()) {
+ endBandSelect();
+ }
+ }
+
+ @Override
+ public void onItemRangeRemoved(int startPosition, int itemCount) {
+ assert(startPosition >= 0);
+ assert(itemCount > 0);
+
+ // TODO: Should update grid model.
+ }
+
+ @Override
+ public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+ throw new UnsupportedOperationException();
+ }
+ });
+
+ mGridListener = new GridModel.OnSelectionChangedListener() {
+
+ @Override
+ public void onSelectionChanged(Set<String> updatedSelection) {
+ BandController.this.onSelectionChanged(updatedSelection);
+ }
+
+ @Override
+ public boolean onBeforeItemStateChange(String id, boolean nextState) {
+ return BandController.this.onBeforeItemStateChange(id, nextState);
+ }
+ };
+
+ mModelBuilder = new Runnable() {
+ @Override
+ public void run() {
+ mModel = new GridModel(mEnvironment, mAdapter);
+ mModel.addOnSelectionChangedListener(mGridListener);
+ }
+ };
+ }
+
+ void bindSelection(Selection selection) {
+ mSelection = selection;
+ }
+
+ private boolean handleEvent(MotionInputEvent e) {
+ if (!e.isMouseEvent() && isActive()) {
+ // Weird things happen if we keep up band select
+ // when touch events happen.
+ endBandSelect();
+ return false;
+ }
+
+ // b/23793622 notes the fact that we *never* receive ACTION_DOWN
+ // events in onTouchEvent. Where it not for this issue, we'd
+ // push start handling down into handleInputEvent.
+ if (shouldStart(e)) {
+ // endBandSelect is handled in handleInputEvent.
+ startBandSelect(e.getOrigin());
+ } else if (isActive() && e.isActionUp()) {
+ // Same issue here w b/23793622. The ACTION_UP event
+ // is only evert dispatched to onTouchEvent when
+ // there is some associated motion. If a user taps
+ // mouse, but doesn't move, then band select gets
+ // started BUT not ended. Causing phantom
+ // bands to appear when the user later clicks to start
+ // band select.
+ if (e.isMouseEvent()) {
+ processInputEvent(e);
+ }
+ }
+
+ return isActive();
+ }
+
+ private boolean isActive() {
+ return mModel != null;
+ }
+
+ /**
+ * Handle a change in layout by cleaning up and getting rid of the old model and creating
+ * a new model which will track the new layout.
+ */
+ public void handleLayoutChanged() {
+ if (mModel != null) {
+ mModel.removeOnSelectionChangedListener(mGridListener);
+ mModel.stopListening();
+
+ // build a new model, all fresh and happy.
+ mModelBuilder.run();
+ }
+ }
+
+ boolean shouldStart(MotionInputEvent e) {
+ return !isActive()
+ && e.isActionDown() // the initial button press
+ && mAdapter.getItemCount() > 0
+ && e.getItemPosition() == RecyclerView.NO_ID; // in empty space
+ }
+
+ boolean shouldStop(InputEvent input) {
+ return isActive()
+ && input.isMouseEvent()
+ && input.isActionUp();
+ }
+
+ /**
+ * Processes a MotionEvent by starting, ending, or resizing the band select overlay.
+ * @param input
+ */
+ private void processInputEvent(InputEvent input) {
+ assert(input.isMouseEvent());
+
+ if (shouldStop(input)) {
+ endBandSelect();
+ return;
+ }
+
+ // We shouldn't get any events in this method when band select is not active,
+ // but it turns some guests show up late to the party.
+ if (!isActive()) {
+ return;
+ }
+
+ mCurrentPosition = input.getOrigin();
+ mModel.resizeSelection(input.getOrigin());
+ scrollViewIfNecessary();
+ resizeBandSelectRectangle();
+ }
+
+ /**
+ * Starts band select by adding the drawable to the RecyclerView's overlay.
+ */
+ private void startBandSelect(Point origin) {
+ if (DEBUG) Log.d(TAG, "Starting band select @ " + origin);
+
+ mOrigin = origin;
+ mModelBuilder.run(); // Creates a new selection model.
+ mModel.startSelection(mOrigin);
+ }
+
+ /**
+ * Scrolls the view if necessary.
+ */
+ private void scrollViewIfNecessary() {
+ mEnvironment.removeCallback(mViewScroller);
+ mViewScroller.run();
+ mEnvironment.invalidateView();
+ }
+
+ /**
+ * Resizes the band select rectangle by using the origin and the current pointer position as
+ * two opposite corners of the selection.
+ */
+ private void resizeBandSelectRectangle() {
+ mBounds = new Rect(Math.min(mOrigin.x, mCurrentPosition.x),
+ Math.min(mOrigin.y, mCurrentPosition.y),
+ Math.max(mOrigin.x, mCurrentPosition.x),
+ Math.max(mOrigin.y, mCurrentPosition.y));
+ mEnvironment.showBand(mBounds);
+ }
+
+ /**
+ * Ends band select by removing the overlay.
+ */
+ private void endBandSelect() {
+ if (DEBUG) Log.d(TAG, "Ending band select.");
+
+ mEnvironment.hideBand();
+ mSelection.applyProvisionalSelection();
+ mModel.endSelection();
+ int firstSelected = mModel.getPositionNearestOrigin();
+ if (firstSelected != NOT_SET) {
+ if (mSelection.contains(mAdapter.getModelId(firstSelected))) {
+ // TODO: firstSelected should really be lastSelected, we want to anchor the item
+ // where the mouse-up occurred.
+ mSelectionManager.setSelectionRangeBegin(firstSelected);
+ } else {
+ // TODO: Check if this is really happening.
+ Log.w(TAG, "First selected by band is NOT in selection!");
+ }
+ }
+
+ mModel = null;
+ mOrigin = null;
+ }
+
+ private void onSelectionChanged(Set<String> updatedSelection) {
+ Map<String, Boolean> delta = mSelection.setProvisionalSelection(updatedSelection);
+ for (Map.Entry<String, Boolean> entry: delta.entrySet()) {
+ mSelectionManager.notifyItemStateChanged(entry.getKey(), entry.getValue());
+ }
+ mSelectionManager.notifySelectionChanged();
+ }
+
+ private boolean onBeforeItemStateChange(String id, boolean nextState) {
+ return mSelectionManager.notifyBeforeItemStateChange(id, nextState);
+ }
+
+ private class ViewScroller implements Runnable {
+ /**
+ * The number of milliseconds of scrolling at which scroll speed continues to increase.
+ * At first, the scroll starts slowly; then, the rate of scrolling increases until it
+ * reaches its maximum value at after this many milliseconds.
+ */
+ private static final long SCROLL_ACCELERATION_LIMIT_TIME_MS = 2000;
+
+ @Override
+ public void run() {
+ // Compute the number of pixels the pointer's y-coordinate is past the view.
+ // Negative values mean the pointer is at or before the top of the view, and
+ // positive values mean that the pointer is at or after the bottom of the view. Note
+ // that one additional pixel is added here so that the view still scrolls when the
+ // pointer is exactly at the top or bottom.
+ int pixelsPastView = 0;
+ if (mCurrentPosition.y <= 0) {
+ pixelsPastView = mCurrentPosition.y - 1;
+ } else if (mCurrentPosition.y >= mEnvironment.getHeight() - 1) {
+ pixelsPastView = mCurrentPosition.y - mEnvironment.getHeight() + 1;
+ }
+
+ if (!isActive() || pixelsPastView == 0) {
+ // If band selection is inactive, or if it is active but not at the edge of the
+ // view, no scrolling is necessary.
+ mScrollStartTime = NOT_SET;
+ return;
+ }
+
+ if (mScrollStartTime == NOT_SET) {
+ // If the pointer was previously not at the edge of the view but now is, set the
+ // start time for the scroll.
+ mScrollStartTime = System.currentTimeMillis();
+ }
+
+ // Compute the number of pixels to scroll, and scroll that many pixels.
+ final int numPixels = computeScrollDistance(
+ pixelsPastView, System.currentTimeMillis() - mScrollStartTime);
+ mEnvironment.scrollBy(numPixels);
+
+ mEnvironment.removeCallback(mViewScroller);
+ mEnvironment.runAtNextFrame(this);
+ }
+
+ /**
+ * Computes the number of pixels to scroll based on how far the pointer is past the end
+ * of the view and how long it has been there. Roughly based on ItemTouchHelper's
+ * algorithm for computing the number of pixels to scroll when an item is dragged to the
+ * end of a {@link RecyclerView}.
+ * @param pixelsPastView
+ * @param scrollDuration
+ * @return
+ */
+ private int computeScrollDistance(int pixelsPastView, long scrollDuration) {
+ final int maxScrollStep = mEnvironment.getHeight();
+ final int direction = (int) Math.signum(pixelsPastView);
+ final int absPastView = Math.abs(pixelsPastView);
+
+ // Calculate the ratio of how far out of the view the pointer currently resides to
+ // the entire height of the view.
+ final float outOfBoundsRatio = Math.min(
+ 1.0f, (float) absPastView / mEnvironment.getHeight());
+ // Interpolate this ratio and use it to compute the maximum scroll that should be
+ // possible for this step.
+ final float cappedScrollStep =
+ direction * maxScrollStep * smoothOutOfBoundsRatio(outOfBoundsRatio);
+
+ // Likewise, calculate the ratio of the time spent in the scroll to the limit.
+ final float timeRatio = Math.min(
+ 1.0f, (float) scrollDuration / SCROLL_ACCELERATION_LIMIT_TIME_MS);
+ // Interpolate this ratio and use it to compute the final number of pixels to
+ // scroll.
+ final int numPixels = (int) (cappedScrollStep * smoothTimeRatio(timeRatio));
+
+ // If the final number of pixels to scroll ends up being 0, the view should still
+ // scroll at least one pixel.
+ return numPixels != 0 ? numPixels : direction;
+ }
+
+ /**
+ * Interpolates the given out of bounds ratio on a curve which starts at (0,0) and ends
+ * at (1,1) and quickly approaches 1 near the start of that interval. This ensures that
+ * drags that are at the edge or barely past the edge of the view still cause sufficient
+ * scrolling. The equation y=(x-1)^5+1 is used, but this could also be tweaked if
+ * needed.
+ * @param ratio A ratio which is in the range [0, 1].
+ * @return A "smoothed" value, also in the range [0, 1].
+ */
+ private float smoothOutOfBoundsRatio(float ratio) {
+ return (float) Math.pow(ratio - 1.0f, 5) + 1.0f;
+ }
+
+ /**
+ * Interpolates the given time ratio on a curve which starts at (0,0) and ends at (1,1)
+ * and stays close to 0 for most input values except those very close to 1. This ensures
+ * that scrolls start out very slowly but speed up drastically after the scroll has been
+ * in progress close to SCROLL_ACCELERATION_LIMIT_TIME_MS. The equation y=x^5 is used,
+ * but this could also be tweaked if needed.
+ * @param ratio A ratio which is in the range [0, 1].
+ * @return A "smoothed" value, also in the range [0, 1].
+ */
+ private float smoothTimeRatio(float ratio) {
+ return (float) Math.pow(ratio, 5);
+ }
+ };
+
+ @Override
+ public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+ if (!isActive()) {
+ return;
+ }
+
+ // Adjust the y-coordinate of the origin the opposite number of pixels so that the
+ // origin remains in the same place relative to the view's items.
+ mOrigin.y -= dy;
+ resizeBandSelectRectangle();
+ }
+
+ /**
+ * Provides a band selection item model for views within a RecyclerView. This class queries the
+ * RecyclerView to determine where its items are placed; then, once band selection is underway,
+ * it alerts listeners of which items are covered by the selections.
+ */
+ @VisibleForTesting
+ static final class GridModel extends RecyclerView.OnScrollListener {
+
+ public static final int NOT_SET = -1;
+
+ // Enum values used to determine the corner at which the origin is located within the
+ private static final int UPPER = 0x00;
+ private static final int LOWER = 0x01;
+ private static final int LEFT = 0x00;
+ private static final int RIGHT = 0x02;
+ private static final int UPPER_LEFT = UPPER | LEFT;
+ private static final int UPPER_RIGHT = UPPER | RIGHT;
+ private static final int LOWER_LEFT = LOWER | LEFT;
+ private static final int LOWER_RIGHT = LOWER | RIGHT;
+
+ private final SelectionEnvironment mHelper;
+ private final DocumentsAdapter mAdapter;
+
+ private final List<GridModel.OnSelectionChangedListener> mOnSelectionChangedListeners =
+ new ArrayList<>();
+
+ // Map from the x-value of the left side of a SparseBooleanArray of adapter positions, keyed
+ // by their y-offset. For example, if the first column of the view starts at an x-value of 5,
+ // mColumns.get(5) would return an array of positions in that column. Within that array, the
+ // value for key y is the adapter position for the item whose y-offset is y.
+ private final SparseArray<SparseIntArray> mColumns = new SparseArray<>();
+
+ // List of limits along the x-axis (columns).
+ // This list is sorted from furthest left to furthest right.
+ private final List<GridModel.Limits> mColumnBounds = new ArrayList<>();
+
+ // List of limits along the y-axis (rows). Note that this list only contains items which
+ // have been in the viewport.
+ private final List<GridModel.Limits> mRowBounds = new ArrayList<>();
+
+ // The adapter positions which have been recorded so far.
+ private final SparseBooleanArray mKnownPositions = new SparseBooleanArray();
+
+ // Array passed to registered OnSelectionChangedListeners. One array is created and reused
+ // throughout the lifetime of the object.
+ private final Set<String> mSelection = new HashSet<>();
+
+ // The current pointer (in absolute positioning from the top of the view).
+ private Point mPointer = null;
+
+ // The bounds of the band selection.
+ private RelativePoint mRelativeOrigin;
+ private RelativePoint mRelativePointer;
+
+ private boolean mIsActive;
+
+ // Tracks where the band select originated from. This is used to determine where selections
+ // should expand from when Shift+click is used.
+ private int mPositionNearestOrigin = NOT_SET;
+
+ GridModel(SelectionEnvironment helper, DocumentsAdapter adapter) {
+ mHelper = helper;
+ mAdapter = adapter;
+ mHelper.addOnScrollListener(this);
+ }
+
+ /**
+ * Stops listening to the view's scrolls. Call this function before discarding a
+ * BandSelecModel object to prevent memory leaks.
+ */
+ void stopListening() {
+ mHelper.removeOnScrollListener(this);
+ }
+
+ /**
+ * Start a band select operation at the given point.
+ * @param relativeOrigin The origin of the band select operation, relative to the viewport.
+ * For example, if the view is scrolled to the bottom, the top-left of the viewport
+ * would have a relative origin of (0, 0), even though its absolute point has a higher
+ * y-value.
+ */
+ void startSelection(Point relativeOrigin) {
+ recordVisibleChildren();
+ if (isEmpty()) {
+ // The selection band logic works only if there is at least one visible child.
+ return;
+ }
+
+ mIsActive = true;
+ mPointer = mHelper.createAbsolutePoint(relativeOrigin);
+ mRelativeOrigin = new RelativePoint(mPointer);
+ mRelativePointer = new RelativePoint(mPointer);
+ computeCurrentSelection();
+ notifyListeners();
+ }
+
+ /**
+ * Resizes the selection by adjusting the pointer (i.e., the corner of the selection
+ * opposite the origin.
+ * @param relativePointer The pointer (opposite of the origin) of the band select operation,
+ * relative to the viewport. For example, if the view is scrolled to the bottom, the
+ * top-left of the viewport would have a relative origin of (0, 0), even though its
+ * absolute point has a higher y-value.
+ */
+ @VisibleForTesting
+ void resizeSelection(Point relativePointer) {
+ mPointer = mHelper.createAbsolutePoint(relativePointer);
+ updateModel();
+ }
+
+ /**
+ * Ends the band selection.
+ */
+ void endSelection() {
+ mIsActive = false;
+ }
+
+ /**
+ * @return The adapter position for the item nearest the origin corresponding to the latest
+ * band select operation, or NOT_SET if the selection did not cover any items.
+ */
+ int getPositionNearestOrigin() {
+ return mPositionNearestOrigin;
+ }
+
+ @Override
+ public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+ if (!mIsActive) {
+ return;
+ }
+
+ mPointer.x += dx;
+ mPointer.y += dy;
+ recordVisibleChildren();
+ updateModel();
+ }
+
+ /**
+ * Queries the view for all children and records their location metadata.
+ */
+ private void recordVisibleChildren() {
+ for (int i = 0; i < mHelper.getVisibleChildCount(); i++) {
+ int adapterPosition = mHelper.getAdapterPositionAt(i);
+ // Sometimes the view is not attached, as we notify the multi selection manager
+ // 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) &&
+ !mKnownPositions.get(adapterPosition)) {
+ mKnownPositions.put(adapterPosition, true);
+ recordItemData(mHelper.getAbsoluteRectForChildViewAt(i), adapterPosition);
+ }
+ }
+ }
+
+ /**
+ * Checks if there are any recorded children.
+ */
+ private boolean isEmpty() {
+ return mColumnBounds.size() == 0 || mRowBounds.size() == 0;
+ }
+
+ /**
+ * Updates the limits lists and column map with the given item metadata.
+ * @param absoluteChildRect The absolute rectangle for the child view being processed.
+ * @param adapterPosition The position of the child view being processed.
+ */
+ private void recordItemData(Rect absoluteChildRect, int adapterPosition) {
+ if (mColumnBounds.size() != mHelper.getColumnCount()) {
+ // If not all x-limits have been recorded, record this one.
+ recordLimits(
+ mColumnBounds, new Limits(absoluteChildRect.left, absoluteChildRect.right));
+ }
+
+ recordLimits(mRowBounds, new Limits(absoluteChildRect.top, absoluteChildRect.bottom));
+
+ SparseIntArray columnList = mColumns.get(absoluteChildRect.left);
+ if (columnList == null) {
+ columnList = new SparseIntArray();
+ mColumns.put(absoluteChildRect.left, columnList);
+ }
+ columnList.put(absoluteChildRect.top, adapterPosition);
+ }
+
+ /**
+ * Ensures limits exists within the sorted list limitsList, and adds it to the list if it
+ * does not exist.
+ */
+ private void recordLimits(List<GridModel.Limits> limitsList, GridModel.Limits limits) {
+ int index = Collections.binarySearch(limitsList, limits);
+ if (index < 0) {
+ limitsList.add(~index, limits);
+ }
+ }
+
+ /**
+ * Handles a moved pointer; this function determines whether the pointer movement resulted
+ * in a selection change and, if it has, notifies listeners of this change.
+ */
+ private void updateModel() {
+ RelativePoint old = mRelativePointer;
+ mRelativePointer = new RelativePoint(mPointer);
+ if (old != null && mRelativePointer.equals(old)) {
+ return;
+ }
+
+ computeCurrentSelection();
+ notifyListeners();
+ }
+
+ /**
+ * Computes the currently-selected items.
+ */
+ private void computeCurrentSelection() {
+ if (areItemsCoveredByBand(mRelativePointer, mRelativeOrigin)) {
+ updateSelection(computeBounds());
+ } else {
+ mSelection.clear();
+ mPositionNearestOrigin = NOT_SET;
+ }
+ }
+
+ /**
+ * Notifies all listeners of a selection change. Note that this function simply passes
+ * mSelection, so computeCurrentSelection() should be called before this
+ * function.
+ */
+ private void notifyListeners() {
+ for (GridModel.OnSelectionChangedListener listener : mOnSelectionChangedListeners) {
+ listener.onSelectionChanged(mSelection);
+ }
+ }
+
+ /**
+ * @param rect Rectangle including all covered items.
+ */
+ private void updateSelection(Rect rect) {
+ int columnStart =
+ Collections.binarySearch(mColumnBounds, new Limits(rect.left, rect.left));
+ assert(columnStart >= 0);
+ int columnEnd = columnStart;
+
+ for (int i = columnStart; i < mColumnBounds.size()
+ && mColumnBounds.get(i).lowerLimit <= rect.right; i++) {
+ columnEnd = i;
+ }
+
+ int rowStart = Collections.binarySearch(mRowBounds, new Limits(rect.top, rect.top));
+ if (rowStart < 0) {
+ mPositionNearestOrigin = NOT_SET;
+ return;
+ }
+
+ int rowEnd = rowStart;
+ for (int i = rowStart; i < mRowBounds.size()
+ && mRowBounds.get(i).lowerLimit <= rect.bottom; i++) {
+ rowEnd = i;
+ }
+
+ updateSelection(columnStart, columnEnd, rowStart, rowEnd);
+ }
+
+ /**
+ * Computes the selection given the previously-computed start- and end-indices for each
+ * row and column.
+ */
+ private void updateSelection(
+ int columnStartIndex, int columnEndIndex, int rowStartIndex, int rowEndIndex) {
+ if (DEBUG) Log.d(TAG, String.format("updateSelection: %d, %d, %d, %d",
+ columnStartIndex, columnEndIndex, rowStartIndex, rowEndIndex));
+
+ mSelection.clear();
+ for (int column = columnStartIndex; column <= columnEndIndex; column++) {
+ SparseIntArray items = mColumns.get(mColumnBounds.get(column).lowerLimit);
+ for (int row = rowStartIndex; row <= rowEndIndex; row++) {
+ // The default return value for SparseIntArray.get is 0, which is a valid
+ // position. Use a sentry value to prevent erroneously selecting item 0.
+ final int rowKey = mRowBounds.get(row).lowerLimit;
+ int position = items.get(rowKey, NOT_SET);
+ if (position != NOT_SET) {
+ String id = mAdapter.getModelId(position);
+ if (id != null) {
+ // The adapter inserts items for UI layout purposes that aren't associated
+ // with files. Those will have a null model ID. Don't select them.
+ if (canSelect(id)) {
+ mSelection.add(id);
+ }
+ }
+ if (isPossiblePositionNearestOrigin(column, columnStartIndex, columnEndIndex,
+ row, rowStartIndex, rowEndIndex)) {
+ // If this is the position nearest the origin, record it now so that it
+ // can be returned by endSelection() later.
+ mPositionNearestOrigin = position;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * @return True if the item is selectable.
+ */
+ private boolean canSelect(String id) {
+ // TODO: Simplify the logic, so the check whether we can select is done in one place.
+ // Consider injecting FragmentTuner, or move the checks from MultiSelectManager to
+ // Selection.
+ for (GridModel.OnSelectionChangedListener listener : mOnSelectionChangedListeners) {
+ if (!listener.onBeforeItemStateChange(id, true)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * @return Returns true if the position is the nearest to the origin, or, in the case of the
+ * lower-right corner, whether it is possible that the position is the nearest to the
+ * origin. See comment below for reasoning for this special case.
+ */
+ private boolean isPossiblePositionNearestOrigin(int columnIndex, int columnStartIndex,
+ int columnEndIndex, int rowIndex, int rowStartIndex, int rowEndIndex) {
+ int corner = computeCornerNearestOrigin();
+ switch (corner) {
+ case UPPER_LEFT:
+ return columnIndex == columnStartIndex && rowIndex == rowStartIndex;
+ case UPPER_RIGHT:
+ return columnIndex == columnEndIndex && rowIndex == rowStartIndex;
+ case LOWER_LEFT:
+ return columnIndex == columnStartIndex && rowIndex == rowEndIndex;
+ case LOWER_RIGHT:
+ // Note that in some cases, the last row will not have as many items as there
+ // are columns (e.g., if there are 4 items and 3 columns, the second row will
+ // only have one item in the first column). This function is invoked for each
+ // position from left to right, so return true for any position in the bottom
+ // row and only the right-most position in the bottom row will be recorded.
+ return rowIndex == rowEndIndex;
+ default:
+ throw new RuntimeException("Invalid corner type.");
+ }
+ }
+
+ /**
+ * Listener for changes in which items have been band selected.
+ */
+ static interface OnSelectionChangedListener {
+ public void onSelectionChanged(Set<String> updatedSelection);
+ public boolean onBeforeItemStateChange(String id, boolean nextState);
+ }
+
+ void addOnSelectionChangedListener(GridModel.OnSelectionChangedListener listener) {
+ mOnSelectionChangedListeners.add(listener);
+ }
+
+ void removeOnSelectionChangedListener(GridModel.OnSelectionChangedListener listener) {
+ mOnSelectionChangedListeners.remove(listener);
+ }
+
+ /**
+ * Limits of a view item. For example, if an item's left side is at x-value 5 and its right side
+ * is at x-value 10, the limits would be from 5 to 10. Used to record the left- and right sides
+ * of item columns and the top- and bottom sides of item rows so that it can be determined
+ * whether the pointer is located within the bounds of an item.
+ */
+ private static class Limits implements Comparable<GridModel.Limits> {
+ int lowerLimit;
+ int upperLimit;
+
+ Limits(int lowerLimit, int upperLimit) {
+ this.lowerLimit = lowerLimit;
+ this.upperLimit = upperLimit;
+ }
+
+ @Override
+ public int compareTo(GridModel.Limits other) {
+ return lowerLimit - other.lowerLimit;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof GridModel.Limits)) {
+ return false;
+ }
+
+ return ((GridModel.Limits) other).lowerLimit == lowerLimit &&
+ ((GridModel.Limits) other).upperLimit == upperLimit;
+ }
+
+ @Override
+ public String toString() {
+ return "(" + lowerLimit + ", " + upperLimit + ")";
+ }
+ }
+
+ /**
+ * The location of a coordinate relative to items. This class represents a general area of the
+ * view as it relates to band selection rather than an explicit point. For example, two
+ * different points within an item are considered to have the same "location" because band
+ * selection originating within the item would select the same items no matter which point
+ * was used. Same goes for points between items as well as those at the very beginning or end
+ * of the view.
+ *
+ * Tracking a coordinate (e.g., an x-value) as a CoordinateLocation instead of as an int has the
+ * advantage of tying the value to the Limits of items along that axis. This allows easy
+ * selection of items within those Limits as opposed to a search through every item to see if a
+ * given coordinate value falls within those Limits.
+ */
+ private static class RelativeCoordinate
+ implements Comparable<GridModel.RelativeCoordinate> {
+ /**
+ * Location describing points after the last known item.
+ */
+ static final int AFTER_LAST_ITEM = 0;
+
+ /**
+ * Location describing points before the first known item.
+ */
+ static final int BEFORE_FIRST_ITEM = 1;
+
+ /**
+ * Location describing points between two items.
+ */
+ static final int BETWEEN_TWO_ITEMS = 2;
+
+ /**
+ * Location describing points within the limits of one item.
+ */
+ static final int WITHIN_LIMITS = 3;
+
+ /**
+ * The type of this coordinate, which is one of AFTER_LAST_ITEM, BEFORE_FIRST_ITEM,
+ * BETWEEN_TWO_ITEMS, or WITHIN_LIMITS.
+ */
+ final int type;
+
+ /**
+ * The limits before the coordinate; only populated when type == WITHIN_LIMITS or type ==
+ * BETWEEN_TWO_ITEMS.
+ */
+ GridModel.Limits limitsBeforeCoordinate;
+
+ /**
+ * The limits after the coordinate; only populated when type == BETWEEN_TWO_ITEMS.
+ */
+ GridModel.Limits limitsAfterCoordinate;
+
+ // Limits of the first known item; only populated when type == BEFORE_FIRST_ITEM.
+ GridModel.Limits mFirstKnownItem;
+ // Limits of the last known item; only populated when type == AFTER_LAST_ITEM.
+ GridModel.Limits mLastKnownItem;
+
+ /**
+ * @param limitsList The sorted limits list for the coordinate type. If this
+ * CoordinateLocation is an x-value, mXLimitsList should be passed; otherwise,
+ * mYLimitsList should be pased.
+ * @param value The coordinate value.
+ */
+ RelativeCoordinate(List<GridModel.Limits> limitsList, int value) {
+ int index = Collections.binarySearch(limitsList, new Limits(value, value));
+
+ if (index >= 0) {
+ this.type = WITHIN_LIMITS;
+ this.limitsBeforeCoordinate = limitsList.get(index);
+ } else if (~index == 0) {
+ this.type = BEFORE_FIRST_ITEM;
+ this.mFirstKnownItem = limitsList.get(0);
+ } else if (~index == limitsList.size()) {
+ GridModel.Limits lastLimits = limitsList.get(limitsList.size() - 1);
+ if (lastLimits.lowerLimit <= value && value <= lastLimits.upperLimit) {
+ this.type = WITHIN_LIMITS;
+ this.limitsBeforeCoordinate = lastLimits;
+ } else {
+ this.type = AFTER_LAST_ITEM;
+ this.mLastKnownItem = lastLimits;
+ }
+ } else {
+ GridModel.Limits limitsBeforeIndex = limitsList.get(~index - 1);
+ if (limitsBeforeIndex.lowerLimit <= value && value <= limitsBeforeIndex.upperLimit) {
+ this.type = WITHIN_LIMITS;
+ this.limitsBeforeCoordinate = limitsList.get(~index - 1);
+ } else {
+ this.type = BETWEEN_TWO_ITEMS;
+ this.limitsBeforeCoordinate = limitsList.get(~index - 1);
+ this.limitsAfterCoordinate = limitsList.get(~index);
+ }
+ }
+ }
+
+ int toComparisonValue() {
+ if (type == BEFORE_FIRST_ITEM) {
+ return mFirstKnownItem.lowerLimit - 1;
+ } else if (type == AFTER_LAST_ITEM) {
+ return mLastKnownItem.upperLimit + 1;
+ } else if (type == BETWEEN_TWO_ITEMS) {
+ return limitsBeforeCoordinate.upperLimit + 1;
+ } else {
+ return limitsBeforeCoordinate.lowerLimit;
+ }
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof GridModel.RelativeCoordinate)) {
+ return false;
+ }
+
+ GridModel.RelativeCoordinate otherCoordinate = (GridModel.RelativeCoordinate) other;
+ return toComparisonValue() == otherCoordinate.toComparisonValue();
+ }
+
+ @Override
+ public int compareTo(GridModel.RelativeCoordinate other) {
+ return toComparisonValue() - other.toComparisonValue();
+ }
+ }
+
+ /**
+ * The location of a point relative to the Limits of nearby items; consists of both an x- and
+ * y-RelativeCoordinateLocation.
+ */
+ private class RelativePoint {
+ final GridModel.RelativeCoordinate xLocation;
+ final GridModel.RelativeCoordinate yLocation;
+
+ RelativePoint(Point point) {
+ this.xLocation = new RelativeCoordinate(mColumnBounds, point.x);
+ this.yLocation = new RelativeCoordinate(mRowBounds, point.y);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof RelativePoint)) {
+ return false;
+ }
+
+ RelativePoint otherPoint = (RelativePoint) other;
+ return xLocation.equals(otherPoint.xLocation) && yLocation.equals(otherPoint.yLocation);
+ }
+ }
+
+ /**
+ * Generates a rectangle which contains the items selected by the pointer and origin.
+ * @return The rectangle, or null if no items were selected.
+ */
+ private Rect computeBounds() {
+ Rect rect = new Rect();
+ rect.left = getCoordinateValue(
+ min(mRelativeOrigin.xLocation, mRelativePointer.xLocation),
+ mColumnBounds,
+ true);
+ rect.right = getCoordinateValue(
+ max(mRelativeOrigin.xLocation, mRelativePointer.xLocation),
+ mColumnBounds,
+ false);
+ rect.top = getCoordinateValue(
+ min(mRelativeOrigin.yLocation, mRelativePointer.yLocation),
+ mRowBounds,
+ true);
+ rect.bottom = getCoordinateValue(
+ max(mRelativeOrigin.yLocation, mRelativePointer.yLocation),
+ mRowBounds,
+ false);
+ return rect;
+ }
+
+ /**
+ * Computes the corner of the selection nearest the origin.
+ * @return
+ */
+ private int computeCornerNearestOrigin() {
+ int cornerValue = 0;
+
+ if (mRelativeOrigin.yLocation ==
+ min(mRelativeOrigin.yLocation, mRelativePointer.yLocation)) {
+ cornerValue |= UPPER;
+ } else {
+ cornerValue |= LOWER;
+ }
+
+ if (mRelativeOrigin.xLocation ==
+ min(mRelativeOrigin.xLocation, mRelativePointer.xLocation)) {
+ cornerValue |= LEFT;
+ } else {
+ cornerValue |= RIGHT;
+ }
+
+ return cornerValue;
+ }
+
+ private GridModel.RelativeCoordinate min(GridModel.RelativeCoordinate first, GridModel.RelativeCoordinate second) {
+ return first.compareTo(second) < 0 ? first : second;
+ }
+
+ private GridModel.RelativeCoordinate max(GridModel.RelativeCoordinate first, GridModel.RelativeCoordinate second) {
+ return first.compareTo(second) > 0 ? first : second;
+ }
+
+ /**
+ * @return The absolute coordinate (i.e., the x- or y-value) of the given relative
+ * coordinate.
+ */
+ private int getCoordinateValue(GridModel.RelativeCoordinate coordinate,
+ List<GridModel.Limits> limitsList, boolean isStartOfRange) {
+ switch (coordinate.type) {
+ case RelativeCoordinate.BEFORE_FIRST_ITEM:
+ return limitsList.get(0).lowerLimit;
+ case RelativeCoordinate.AFTER_LAST_ITEM:
+ return limitsList.get(limitsList.size() - 1).upperLimit;
+ case RelativeCoordinate.BETWEEN_TWO_ITEMS:
+ if (isStartOfRange) {
+ return coordinate.limitsAfterCoordinate.lowerLimit;
+ } else {
+ return coordinate.limitsBeforeCoordinate.upperLimit;
+ }
+ case RelativeCoordinate.WITHIN_LIMITS:
+ return coordinate.limitsBeforeCoordinate.lowerLimit;
+ }
+
+ throw new RuntimeException("Invalid coordinate value.");
+ }
+
+ private boolean areItemsCoveredByBand(
+ RelativePoint first, RelativePoint second) {
+ return doesCoordinateLocationCoverItems(first.xLocation, second.xLocation) &&
+ doesCoordinateLocationCoverItems(first.yLocation, second.yLocation);
+ }
+
+ private boolean doesCoordinateLocationCoverItems(
+ GridModel.RelativeCoordinate pointerCoordinate,
+ GridModel.RelativeCoordinate originCoordinate) {
+ if (pointerCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM &&
+ originCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM) {
+ return false;
+ }
+
+ if (pointerCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM &&
+ originCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM) {
+ return false;
+ }
+
+ if (pointerCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS &&
+ originCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS &&
+ pointerCoordinate.limitsBeforeCoordinate.equals(
+ originCoordinate.limitsBeforeCoordinate) &&
+ pointerCoordinate.limitsAfterCoordinate.equals(
+ originCoordinate.limitsAfterCoordinate)) {
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ /**
+ * Provides functionality for BandController. Exists primarily to tests that are
+ * fully isolated from RecyclerView.
+ */
+ interface SelectionEnvironment {
+ void showBand(Rect rect);
+ void hideBand();
+ void addOnScrollListener(RecyclerView.OnScrollListener listener);
+ void removeOnScrollListener(RecyclerView.OnScrollListener listener);
+ void scrollBy(int dy);
+ int getHeight();
+ void invalidateView();
+ void runAtNextFrame(Runnable r);
+ void removeCallback(Runnable r);
+ Point createAbsolutePoint(Point relativePoint);
+ Rect getAbsoluteRectForChildViewAt(int index);
+ int getAdapterPositionAt(int index);
+ int getColumnCount();
+ 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);
+ }
+
+ /** Recycler view facade implementation backed by good ol' RecyclerView. */
+ private static final class RuntimeSelectionEnvironment implements SelectionEnvironment {
+
+ private final RecyclerView mView;
+ private final Drawable mBand;
+
+ private boolean mIsOverlayShown = false;
+
+ RuntimeSelectionEnvironment(RecyclerView view) {
+ mView = view;
+ mBand = mView.getContext().getTheme().getDrawable(R.drawable.band_select_overlay);
+ }
+
+ @Override
+ public int getAdapterPositionAt(int index) {
+ return mView.getChildAdapterPosition(mView.getChildAt(index));
+ }
+
+ @Override
+ public void addOnScrollListener(RecyclerView.OnScrollListener listener) {
+ mView.addOnScrollListener(listener);
+ }
+
+ @Override
+ public void removeOnScrollListener(RecyclerView.OnScrollListener listener) {
+ mView.removeOnScrollListener(listener);
+ }
+
+ @Override
+ public Point createAbsolutePoint(Point relativePoint) {
+ return new Point(relativePoint.x + mView.computeHorizontalScrollOffset(),
+ relativePoint.y + mView.computeVerticalScrollOffset());
+ }
+
+ @Override
+ public Rect getAbsoluteRectForChildViewAt(int index) {
+ final View child = mView.getChildAt(index);
+ final Rect childRect = new Rect();
+ child.getHitRect(childRect);
+ childRect.left += mView.computeHorizontalScrollOffset();
+ childRect.right += mView.computeHorizontalScrollOffset();
+ childRect.top += mView.computeVerticalScrollOffset();
+ childRect.bottom += mView.computeVerticalScrollOffset();
+ return childRect;
+ }
+
+ @Override
+ public int getChildCount() {
+ return mView.getAdapter().getItemCount();
+ }
+
+ @Override
+ public int getVisibleChildCount() {
+ return mView.getChildCount();
+ }
+
+ @Override
+ public int getColumnCount() {
+ RecyclerView.LayoutManager layoutManager = mView.getLayoutManager();
+ if (layoutManager instanceof GridLayoutManager) {
+ return ((GridLayoutManager) layoutManager).getSpanCount();
+ }
+
+ // Otherwise, it is a list with 1 column.
+ return 1;
+ }
+
+ @Override
+ public int getHeight() {
+ return mView.getHeight();
+ }
+
+ @Override
+ public void invalidateView() {
+ mView.invalidate();
+ }
+
+ @Override
+ public void runAtNextFrame(Runnable r) {
+ mView.postOnAnimation(r);
+ }
+
+ @Override
+ public void removeCallback(Runnable r) {
+ mView.removeCallbacks(r);
+ }
+
+ @Override
+ public void scrollBy(int dy) {
+ mView.scrollBy(0, dy);
+ }
+
+ @Override
+ public void showBand(Rect rect) {
+ mBand.setBounds(rect);
+
+ if (!mIsOverlayShown) {
+ mView.getOverlay().add(mBand);
+ }
+ }
+
+ @Override
+ public void hideBand() {
+ mView.getOverlay().remove(mBand);
+ }
+
+ @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/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index f3df57f..efe76df 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -177,6 +177,8 @@
// Save selection found during creation so it can be restored during directory loading.
private Selection mSelection = null;
private boolean mSearchMode = false;
+
+ private @Nullable BandController mBandController;
private @Nullable ActionMode mActionMode;
private DirectoryDragListener mOnDragListener;
@@ -299,6 +301,10 @@
: MultiSelectManager.MODE_SINGLE,
null);
+ if (state.allowMultiple) {
+ mBandController = new BandController(mRecView, mAdapter, mSelectionManager);
+ }
+
mSelectionManager.addCallback(new SelectionModeListener());
mModel = new Model();
@@ -451,7 +457,9 @@
int pad = getDirectoryPadding(mode);
mRecView.setPadding(pad, pad, pad, pad);
mRecView.requestLayout();
- mSelectionManager.handleLayoutChanged(); // RecyclerView doesn't do this for us
+ if (mBandController != null) {
+ mBandController.handleLayoutChanged();
+ }
mIconHelper.setViewMode(mode);
}
diff --git a/src/com/android/documentsui/dirlist/MultiSelectManager.java b/src/com/android/documentsui/dirlist/MultiSelectManager.java
index 8852985..d570bf1 100644
--- a/src/com/android/documentsui/dirlist/MultiSelectManager.java
+++ b/src/com/android/documentsui/dirlist/MultiSelectManager.java
@@ -17,35 +17,21 @@
package com.android.documentsui.dirlist;
import static com.android.documentsui.Shared.DEBUG;
-import static com.android.documentsui.dirlist.ModelBackedDocumentsAdapter.ITEM_TYPE_DIRECTORY;
-import static com.android.documentsui.dirlist.ModelBackedDocumentsAdapter.ITEM_TYPE_DOCUMENT;
import android.annotation.IntDef;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
-import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
-import android.util.SparseArray;
-import android.util.SparseBooleanArray;
-import android.util.SparseIntArray;
-import android.view.MotionEvent;
-import android.view.View;
import com.android.documentsui.Events.InputEvent;
-import com.android.documentsui.Events.MotionInputEvent;
-import com.android.documentsui.R;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -72,16 +58,12 @@
private final Selection mSelection = new Selection();
- private final SelectionEnvironment mEnvironment;
private final DocumentsAdapter mAdapter;
private final List<MultiSelectManager.Callback> mCallbacks = new ArrayList<>(1);
private Range mRanger;
private boolean mSingleSelect;
- @Nullable private BandController mBandManager;
-
-
/**
* @param mode Selection single or multiple selection mode.
* @param initialSelection selection state probably preserved in external state.
@@ -92,31 +74,7 @@
@SelectionMode int mode,
@Nullable Selection initialSelection) {
- this(new RuntimeSelectionEnvironment(recyclerView), adapter, mode, initialSelection);
-
- if (mode == MODE_MULTIPLE) {
- // TODO: Don't load this on low memory devices.
- mBandManager = new BandController();
- }
-
- recyclerView.addOnItemTouchListener(
- new RecyclerView.OnItemTouchListener() {
- @Override
- public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
- if (mBandManager != null) {
- return mBandManager.handleEvent(new MotionInputEvent(e, recyclerView));
- }
- return false;
- }
-
- @Override
- public void onTouchEvent(RecyclerView rv, MotionEvent e) {
- mBandManager.processInputEvent(
- new MotionInputEvent(e, recyclerView));
- }
- @Override
- public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
- });
+ this(adapter, mode, initialSelection);
}
/**
@@ -126,15 +84,12 @@
*/
@VisibleForTesting
MultiSelectManager(
- SelectionEnvironment environment,
DocumentsAdapter adapter,
@SelectionMode int mode,
@Nullable Selection initialSelection) {
- assert(environment != null);
assert(adapter != null);
- mEnvironment = environment;
mAdapter = adapter;
mSingleSelect = mode == MODE_SINGLE;
@@ -154,10 +109,6 @@
// Update the selection to remove any disappeared IDs.
mSelection.cancelProvisionalSelection();
mSelection.intersect(mModelIds);
-
- if (mBandManager != null && mBandManager.isActive()) {
- mBandManager.endBandSelect();
- }
}
@Override
@@ -188,6 +139,11 @@
});
}
+ 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.
@@ -263,12 +219,6 @@
notifySelectionChanged();
}
- public void handleLayoutChanged() {
- if (mBandManager != null) {
- mBandManager.handleLayoutChanged();
- }
- }
-
/**
* Clears the selection, without notifying selection listeners. UI elements still need to be
* notified about state changes so that they can update their appearance.
@@ -511,7 +461,7 @@
return true;
}
- private boolean notifyBeforeItemStateChange(String id, boolean nextState) {
+ boolean notifyBeforeItemStateChange(String id, boolean nextState) {
int lastListener = mCallbacks.size() - 1;
for (int i = lastListener; i > -1; i--) {
if (!mCallbacks.get(i).onBeforeItemStateChange(id, nextState)) {
@@ -525,7 +475,7 @@
* Notifies registered listeners when the selection status of a single item
* (identified by {@code position}) changes.
*/
- private void notifyItemStateChanged(String id, boolean selected) {
+ void notifyItemStateChanged(String id, boolean selected) {
assert(id != null);
int lastListener = mCallbacks.size() - 1;
for (int i = lastListener; i > -1; i--) {
@@ -540,7 +490,7 @@
* is complete, e.g. clearingSelection, or updating the single
* selection from one item to another.
*/
- private void notifySelectionChanged() {
+ void notifySelectionChanged() {
int lastListener = mCallbacks.size() - 1;
for (int i = lastListener; i > -1; i--) {
mCallbacks.get(i).onSelectionChanged();
@@ -920,162 +870,6 @@
};
}
- /**
- * Provides functionality for BandController. Exists primarily to tests that are
- * fully isolated from RecyclerView.
- */
- interface SelectionEnvironment {
- void showBand(Rect rect);
- void hideBand();
- void addOnScrollListener(RecyclerView.OnScrollListener listener);
- void removeOnScrollListener(RecyclerView.OnScrollListener listener);
- void scrollBy(int dy);
- int getHeight();
- void invalidateView();
- void runAtNextFrame(Runnable r);
- void removeCallback(Runnable r);
- Point createAbsolutePoint(Point relativePoint);
- Rect getAbsoluteRectForChildViewAt(int index);
- int getAdapterPositionAt(int index);
- int getColumnCount();
- 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);
- }
-
- /** Recycler view facade implementation backed by good ol' RecyclerView. */
- private static final class RuntimeSelectionEnvironment implements SelectionEnvironment {
-
- private final RecyclerView mView;
- private final Drawable mBand;
-
- private boolean mIsOverlayShown = false;
-
- RuntimeSelectionEnvironment(RecyclerView view) {
- mView = view;
- mBand = mView.getContext().getTheme().getDrawable(R.drawable.band_select_overlay);
- }
-
- @Override
- public int getAdapterPositionAt(int index) {
- return mView.getChildAdapterPosition(mView.getChildAt(index));
- }
-
- @Override
- public void addOnScrollListener(RecyclerView.OnScrollListener listener) {
- mView.addOnScrollListener(listener);
- }
-
- @Override
- public void removeOnScrollListener(RecyclerView.OnScrollListener listener) {
- mView.removeOnScrollListener(listener);
- }
-
- @Override
- public Point createAbsolutePoint(Point relativePoint) {
- return new Point(relativePoint.x + mView.computeHorizontalScrollOffset(),
- relativePoint.y + mView.computeVerticalScrollOffset());
- }
-
- @Override
- public Rect getAbsoluteRectForChildViewAt(int index) {
- final View child = mView.getChildAt(index);
- final Rect childRect = new Rect();
- child.getHitRect(childRect);
- childRect.left += mView.computeHorizontalScrollOffset();
- childRect.right += mView.computeHorizontalScrollOffset();
- childRect.top += mView.computeVerticalScrollOffset();
- childRect.bottom += mView.computeVerticalScrollOffset();
- return childRect;
- }
-
- @Override
- public int getChildCount() {
- return mView.getAdapter().getItemCount();
- }
-
- @Override
- public int getVisibleChildCount() {
- return mView.getChildCount();
- }
-
- @Override
- public int getColumnCount() {
- RecyclerView.LayoutManager layoutManager = mView.getLayoutManager();
- if (layoutManager instanceof GridLayoutManager) {
- return ((GridLayoutManager) layoutManager).getSpanCount();
- }
-
- // Otherwise, it is a list with 1 column.
- return 1;
- }
-
- @Override
- public int getHeight() {
- return mView.getHeight();
- }
-
- @Override
- public void invalidateView() {
- mView.invalidate();
- }
-
- @Override
- public void runAtNextFrame(Runnable r) {
- mView.postOnAnimation(r);
- }
-
- @Override
- public void removeCallback(Runnable r) {
- mView.removeCallbacks(r);
- }
-
- @Override
- public void scrollBy(int dy) {
- mView.scrollBy(0, dy);
- }
-
- @Override
- public void showBand(Rect rect) {
- mBand.setBounds(rect);
-
- if (!mIsOverlayShown) {
- mView.getOverlay().add(mBand);
- }
- }
-
- @Override
- public void hideBand() {
- mView.getOverlay().remove(mBand);
- }
-
- @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;
- }
- }
-
public interface Callback {
/**
* Called when an item is selected or unselected while in selection mode.
@@ -1101,958 +895,4 @@
*/
public void onSelectionChanged();
}
-
- /**
- * 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.
- */
- public class BandController extends RecyclerView.OnScrollListener
- implements GridModel.OnSelectionChangedListener {
-
- private static final int NOT_SET = -1;
-
- private final Runnable mModelBuilder;
-
- @Nullable private Rect mBounds;
- @Nullable private Point mCurrentPosition;
- @Nullable private Point mOrigin;
- @Nullable private GridModel mModel;
-
- // The time at which the current band selection-induced scroll began. If no scroll is in
- // progress, the value is NOT_SET.
- private long mScrollStartTime = NOT_SET;
- private final Runnable mViewScroller = new ViewScroller();
-
- public BandController() {
- mEnvironment.addOnScrollListener(this);
-
- mModelBuilder = new Runnable() {
- @Override
- public void run() {
- mModel = new GridModel(mEnvironment, mAdapter);
- mModel.addOnSelectionChangedListener(BandController.this);
- }
- };
- }
-
- public boolean handleEvent(MotionInputEvent e) {
- // b/23793622 notes the fact that we *never* receive ACTION_DOWN
- // events in onTouchEvent. Where it not for this issue, we'd
- // push start handling down into handleInputEvent.
- if (mBandManager.shouldStart(e)) {
- // endBandSelect is handled in handleInputEvent.
- mBandManager.startBandSelect(e.getOrigin());
- } else if (mBandManager.isActive()
- && e.isMouseEvent()
- && e.isActionUp()) {
- // Same issue here w b/23793622. The ACTION_UP event
- // is only evert dispatched to onTouchEvent when
- // there is some associated motion. If a user taps
- // mouse, but doesn't move, then band select gets
- // started BUT not ended. Causing phantom
- // bands to appear when the user later clicks to start
- // band select.
- mBandManager.processInputEvent(e);
- }
-
- return isActive();
- }
-
- private boolean isActive() {
- return mModel != null;
- }
-
- /**
- * Handle a change in layout by cleaning up and getting rid of the old model and creating
- * a new model which will track the new layout.
- */
- public void handleLayoutChanged() {
- if (mModel != null) {
- mModel.removeOnSelectionChangedListener(this);
- mModel.stopListening();
-
- // build a new model, all fresh and happy.
- mModelBuilder.run();
- }
- }
-
- boolean shouldStart(MotionInputEvent e) {
- return !isActive()
- && e.isMouseEvent() // a mouse
- && e.isActionDown() // the initial button press
- && mAdapter.getItemCount() > 0
- && e.getItemPosition() == RecyclerView.NO_ID; // in empty space
- }
-
- boolean shouldStop(InputEvent input) {
- return isActive()
- && input.isMouseEvent()
- && input.isActionUp();
- }
-
- /**
- * Processes a MotionEvent by starting, ending, or resizing the band select overlay.
- * @param input
- */
- private void processInputEvent(InputEvent input) {
- assert(input.isMouseEvent());
-
- if (shouldStop(input)) {
- endBandSelect();
- return;
- }
-
- // We shouldn't get any events in this method when band select is not active,
- // but it turns some guests show up late to the party.
- if (!isActive()) {
- return;
- }
-
- mCurrentPosition = input.getOrigin();
- mModel.resizeSelection(input.getOrigin());
- scrollViewIfNecessary();
- resizeBandSelectRectangle();
- }
-
- /**
- * Starts band select by adding the drawable to the RecyclerView's overlay.
- */
- private void startBandSelect(Point origin) {
- if (DEBUG) Log.d(TAG, "Starting band select @ " + origin);
-
- mOrigin = origin;
- mModelBuilder.run(); // Creates a new selection model.
- mModel.startSelection(mOrigin);
- }
-
- /**
- * Scrolls the view if necessary.
- */
- private void scrollViewIfNecessary() {
- mEnvironment.removeCallback(mViewScroller);
- mViewScroller.run();
- mEnvironment.invalidateView();
- }
-
- /**
- * Resizes the band select rectangle by using the origin and the current pointer position as
- * two opposite corners of the selection.
- */
- private void resizeBandSelectRectangle() {
- mBounds = new Rect(Math.min(mOrigin.x, mCurrentPosition.x),
- Math.min(mOrigin.y, mCurrentPosition.y),
- Math.max(mOrigin.x, mCurrentPosition.x),
- Math.max(mOrigin.y, mCurrentPosition.y));
- mEnvironment.showBand(mBounds);
- }
-
- /**
- * Ends band select by removing the overlay.
- */
- private void endBandSelect() {
- if (DEBUG) Log.d(TAG, "Ending band select.");
-
- mEnvironment.hideBand();
- mSelection.applyProvisionalSelection();
- mModel.endSelection();
- int firstSelected = mModel.getPositionNearestOrigin();
- if (firstSelected != NOT_SET) {
- if (mSelection.contains(mAdapter.getModelId(firstSelected))) {
- // TODO: firstSelected should really be lastSelected, we want to anchor the item
- // where the mouse-up occurred.
- setSelectionRangeBegin(firstSelected);
- } else {
- // TODO: Check if this is really happening.
- Log.w(TAG, "First selected by band is NOT in selection!");
- }
- }
-
- mModel = null;
- mOrigin = null;
- }
-
- @Override
- public void onSelectionChanged(Set<String> updatedSelection) {
- Map<String, Boolean> delta = mSelection.setProvisionalSelection(updatedSelection);
- for (Map.Entry<String, Boolean> entry: delta.entrySet()) {
- notifyItemStateChanged(entry.getKey(), entry.getValue());
- }
- notifySelectionChanged();
- }
-
- @Override
- public boolean onBeforeItemStateChange(String id, boolean nextState) {
- return notifyBeforeItemStateChange(id, nextState);
- }
-
- private class ViewScroller implements Runnable {
- /**
- * The number of milliseconds of scrolling at which scroll speed continues to increase.
- * At first, the scroll starts slowly; then, the rate of scrolling increases until it
- * reaches its maximum value at after this many milliseconds.
- */
- private static final long SCROLL_ACCELERATION_LIMIT_TIME_MS = 2000;
-
- @Override
- public void run() {
- // Compute the number of pixels the pointer's y-coordinate is past the view.
- // Negative values mean the pointer is at or before the top of the view, and
- // positive values mean that the pointer is at or after the bottom of the view. Note
- // that one additional pixel is added here so that the view still scrolls when the
- // pointer is exactly at the top or bottom.
- int pixelsPastView = 0;
- if (mCurrentPosition.y <= 0) {
- pixelsPastView = mCurrentPosition.y - 1;
- } else if (mCurrentPosition.y >= mEnvironment.getHeight() - 1) {
- pixelsPastView = mCurrentPosition.y - mEnvironment.getHeight() + 1;
- }
-
- if (!isActive() || pixelsPastView == 0) {
- // If band selection is inactive, or if it is active but not at the edge of the
- // view, no scrolling is necessary.
- mScrollStartTime = NOT_SET;
- return;
- }
-
- if (mScrollStartTime == NOT_SET) {
- // If the pointer was previously not at the edge of the view but now is, set the
- // start time for the scroll.
- mScrollStartTime = System.currentTimeMillis();
- }
-
- // Compute the number of pixels to scroll, and scroll that many pixels.
- final int numPixels = computeScrollDistance(
- pixelsPastView, System.currentTimeMillis() - mScrollStartTime);
- mEnvironment.scrollBy(numPixels);
-
- mEnvironment.removeCallback(mViewScroller);
- mEnvironment.runAtNextFrame(this);
- }
-
- /**
- * Computes the number of pixels to scroll based on how far the pointer is past the end
- * of the view and how long it has been there. Roughly based on ItemTouchHelper's
- * algorithm for computing the number of pixels to scroll when an item is dragged to the
- * end of a {@link RecyclerView}.
- * @param pixelsPastView
- * @param scrollDuration
- * @return
- */
- private int computeScrollDistance(int pixelsPastView, long scrollDuration) {
- final int maxScrollStep = mEnvironment.getHeight();
- final int direction = (int) Math.signum(pixelsPastView);
- final int absPastView = Math.abs(pixelsPastView);
-
- // Calculate the ratio of how far out of the view the pointer currently resides to
- // the entire height of the view.
- final float outOfBoundsRatio = Math.min(
- 1.0f, (float) absPastView / mEnvironment.getHeight());
- // Interpolate this ratio and use it to compute the maximum scroll that should be
- // possible for this step.
- final float cappedScrollStep =
- direction * maxScrollStep * smoothOutOfBoundsRatio(outOfBoundsRatio);
-
- // Likewise, calculate the ratio of the time spent in the scroll to the limit.
- final float timeRatio = Math.min(
- 1.0f, (float) scrollDuration / SCROLL_ACCELERATION_LIMIT_TIME_MS);
- // Interpolate this ratio and use it to compute the final number of pixels to
- // scroll.
- final int numPixels = (int) (cappedScrollStep * smoothTimeRatio(timeRatio));
-
- // If the final number of pixels to scroll ends up being 0, the view should still
- // scroll at least one pixel.
- return numPixels != 0 ? numPixels : direction;
- }
-
- /**
- * Interpolates the given out of bounds ratio on a curve which starts at (0,0) and ends
- * at (1,1) and quickly approaches 1 near the start of that interval. This ensures that
- * drags that are at the edge or barely past the edge of the view still cause sufficient
- * scrolling. The equation y=(x-1)^5+1 is used, but this could also be tweaked if
- * needed.
- * @param ratio A ratio which is in the range [0, 1].
- * @return A "smoothed" value, also in the range [0, 1].
- */
- private float smoothOutOfBoundsRatio(float ratio) {
- return (float) Math.pow(ratio - 1.0f, 5) + 1.0f;
- }
-
- /**
- * Interpolates the given time ratio on a curve which starts at (0,0) and ends at (1,1)
- * and stays close to 0 for most input values except those very close to 1. This ensures
- * that scrolls start out very slowly but speed up drastically after the scroll has been
- * in progress close to SCROLL_ACCELERATION_LIMIT_TIME_MS. The equation y=x^5 is used,
- * but this could also be tweaked if needed.
- * @param ratio A ratio which is in the range [0, 1].
- * @return A "smoothed" value, also in the range [0, 1].
- */
- private float smoothTimeRatio(float ratio) {
- return (float) Math.pow(ratio, 5);
- }
- };
-
- @Override
- public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
- if (!isActive()) {
- return;
- }
-
- // Adjust the y-coordinate of the origin the opposite number of pixels so that the
- // origin remains in the same place relative to the view's items.
- mOrigin.y -= dy;
- resizeBandSelectRectangle();
- }
- }
-
- /**
- * Provides a band selection item model for views within a RecyclerView. This class queries the
- * RecyclerView to determine where its items are placed; then, once band selection is underway,
- * it alerts listeners of which items are covered by the selections.
- */
- public static final class GridModel extends RecyclerView.OnScrollListener {
-
- public static final int NOT_SET = -1;
-
- // Enum values used to determine the corner at which the origin is located within the
- private static final int UPPER = 0x00;
- private static final int LOWER = 0x01;
- private static final int LEFT = 0x00;
- private static final int RIGHT = 0x02;
- private static final int UPPER_LEFT = UPPER | LEFT;
- private static final int UPPER_RIGHT = UPPER | RIGHT;
- private static final int LOWER_LEFT = LOWER | LEFT;
- private static final int LOWER_RIGHT = LOWER | RIGHT;
-
- private final SelectionEnvironment mHelper;
- private final DocumentsAdapter mAdapter;
-
- private final List<OnSelectionChangedListener> mOnSelectionChangedListeners =
- new ArrayList<>();
-
- // Map from the x-value of the left side of a SparseBooleanArray of adapter positions, keyed
- // by their y-offset. For example, if the first column of the view starts at an x-value of 5,
- // mColumns.get(5) would return an array of positions in that column. Within that array, the
- // value for key y is the adapter position for the item whose y-offset is y.
- private final SparseArray<SparseIntArray> mColumns = new SparseArray<>();
-
- // List of limits along the x-axis (columns).
- // This list is sorted from furthest left to furthest right.
- private final List<Limits> mColumnBounds = new ArrayList<>();
-
- // List of limits along the y-axis (rows). Note that this list only contains items which
- // have been in the viewport.
- private final List<Limits> mRowBounds = new ArrayList<>();
-
- // The adapter positions which have been recorded so far.
- private final SparseBooleanArray mKnownPositions = new SparseBooleanArray();
-
- // Array passed to registered OnSelectionChangedListeners. One array is created and reused
- // throughout the lifetime of the object.
- private final Set<String> mSelection = new HashSet<>();
-
- // The current pointer (in absolute positioning from the top of the view).
- private Point mPointer = null;
-
- // The bounds of the band selection.
- private RelativePoint mRelativeOrigin;
- private RelativePoint mRelativePointer;
-
- private boolean mIsActive;
-
- // Tracks where the band select originated from. This is used to determine where selections
- // should expand from when Shift+click is used.
- private int mPositionNearestOrigin = NOT_SET;
-
- GridModel(SelectionEnvironment helper, DocumentsAdapter adapter) {
- mHelper = helper;
- mAdapter = adapter;
- mHelper.addOnScrollListener(this);
- }
-
- /**
- * Stops listening to the view's scrolls. Call this function before discarding a
- * BandSelecModel object to prevent memory leaks.
- */
- void stopListening() {
- mHelper.removeOnScrollListener(this);
- }
-
- /**
- * Start a band select operation at the given point.
- * @param relativeOrigin The origin of the band select operation, relative to the viewport.
- * For example, if the view is scrolled to the bottom, the top-left of the viewport
- * would have a relative origin of (0, 0), even though its absolute point has a higher
- * y-value.
- */
- void startSelection(Point relativeOrigin) {
- recordVisibleChildren();
- if (isEmpty()) {
- // The selection band logic works only if there is at least one visible child.
- return;
- }
-
- mIsActive = true;
- mPointer = mHelper.createAbsolutePoint(relativeOrigin);
- mRelativeOrigin = new RelativePoint(mPointer);
- mRelativePointer = new RelativePoint(mPointer);
- computeCurrentSelection();
- notifyListeners();
- }
-
- /**
- * Resizes the selection by adjusting the pointer (i.e., the corner of the selection
- * opposite the origin.
- * @param relativePointer The pointer (opposite of the origin) of the band select operation,
- * relative to the viewport. For example, if the view is scrolled to the bottom, the
- * top-left of the viewport would have a relative origin of (0, 0), even though its
- * absolute point has a higher y-value.
- */
- @VisibleForTesting
- void resizeSelection(Point relativePointer) {
- mPointer = mHelper.createAbsolutePoint(relativePointer);
- updateModel();
- }
-
- /**
- * Ends the band selection.
- */
- void endSelection() {
- mIsActive = false;
- }
-
- /**
- * @return The adapter position for the item nearest the origin corresponding to the latest
- * band select operation, or NOT_SET if the selection did not cover any items.
- */
- int getPositionNearestOrigin() {
- return mPositionNearestOrigin;
- }
-
- @Override
- public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
- if (!mIsActive) {
- return;
- }
-
- mPointer.x += dx;
- mPointer.y += dy;
- recordVisibleChildren();
- updateModel();
- }
-
- /**
- * Queries the view for all children and records their location metadata.
- */
- private void recordVisibleChildren() {
- for (int i = 0; i < mHelper.getVisibleChildCount(); i++) {
- int adapterPosition = mHelper.getAdapterPositionAt(i);
- // Sometimes the view is not attached, as we notify the multi selection manager
- // 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) &&
- !mKnownPositions.get(adapterPosition)) {
- mKnownPositions.put(adapterPosition, true);
- recordItemData(mHelper.getAbsoluteRectForChildViewAt(i), adapterPosition);
- }
- }
- }
-
- /**
- * Checks if there are any recorded children.
- */
- private boolean isEmpty() {
- return mColumnBounds.size() == 0 || mRowBounds.size() == 0;
- }
-
- /**
- * Updates the limits lists and column map with the given item metadata.
- * @param absoluteChildRect The absolute rectangle for the child view being processed.
- * @param adapterPosition The position of the child view being processed.
- */
- private void recordItemData(Rect absoluteChildRect, int adapterPosition) {
- if (mColumnBounds.size() != mHelper.getColumnCount()) {
- // If not all x-limits have been recorded, record this one.
- recordLimits(
- mColumnBounds, new Limits(absoluteChildRect.left, absoluteChildRect.right));
- }
-
- recordLimits(mRowBounds, new Limits(absoluteChildRect.top, absoluteChildRect.bottom));
-
- SparseIntArray columnList = mColumns.get(absoluteChildRect.left);
- if (columnList == null) {
- columnList = new SparseIntArray();
- mColumns.put(absoluteChildRect.left, columnList);
- }
- columnList.put(absoluteChildRect.top, adapterPosition);
- }
-
- /**
- * Ensures limits exists within the sorted list limitsList, and adds it to the list if it
- * does not exist.
- */
- private void recordLimits(List<Limits> limitsList, Limits limits) {
- int index = Collections.binarySearch(limitsList, limits);
- if (index < 0) {
- limitsList.add(~index, limits);
- }
- }
-
- /**
- * Handles a moved pointer; this function determines whether the pointer movement resulted
- * in a selection change and, if it has, notifies listeners of this change.
- */
- private void updateModel() {
- RelativePoint old = mRelativePointer;
- mRelativePointer = new RelativePoint(mPointer);
- if (old != null && mRelativePointer.equals(old)) {
- return;
- }
-
- computeCurrentSelection();
- notifyListeners();
- }
-
- /**
- * Computes the currently-selected items.
- */
- private void computeCurrentSelection() {
- if (areItemsCoveredByBand(mRelativePointer, mRelativeOrigin)) {
- updateSelection(computeBounds());
- } else {
- mSelection.clear();
- mPositionNearestOrigin = NOT_SET;
- }
- }
-
- /**
- * Notifies all listeners of a selection change. Note that this function simply passes
- * mSelection, so computeCurrentSelection() should be called before this
- * function.
- */
- private void notifyListeners() {
- for (OnSelectionChangedListener listener : mOnSelectionChangedListeners) {
- listener.onSelectionChanged(mSelection);
- }
- }
-
- /**
- * @param rect Rectangle including all covered items.
- */
- private void updateSelection(Rect rect) {
- int columnStart =
- Collections.binarySearch(mColumnBounds, new Limits(rect.left, rect.left));
- assert(columnStart >= 0);
- int columnEnd = columnStart;
-
- for (int i = columnStart; i < mColumnBounds.size()
- && mColumnBounds.get(i).lowerLimit <= rect.right; i++) {
- columnEnd = i;
- }
-
- int rowStart = Collections.binarySearch(mRowBounds, new Limits(rect.top, rect.top));
- if (rowStart < 0) {
- mPositionNearestOrigin = NOT_SET;
- return;
- }
-
- int rowEnd = rowStart;
- for (int i = rowStart; i < mRowBounds.size()
- && mRowBounds.get(i).lowerLimit <= rect.bottom; i++) {
- rowEnd = i;
- }
-
- updateSelection(columnStart, columnEnd, rowStart, rowEnd);
- }
-
- /**
- * Computes the selection given the previously-computed start- and end-indices for each
- * row and column.
- */
- private void updateSelection(
- int columnStartIndex, int columnEndIndex, int rowStartIndex, int rowEndIndex) {
- if (DEBUG) Log.d(TAG, String.format("updateSelection: %d, %d, %d, %d",
- columnStartIndex, columnEndIndex, rowStartIndex, rowEndIndex));
-
- mSelection.clear();
- for (int column = columnStartIndex; column <= columnEndIndex; column++) {
- SparseIntArray items = mColumns.get(mColumnBounds.get(column).lowerLimit);
- for (int row = rowStartIndex; row <= rowEndIndex; row++) {
- // The default return value for SparseIntArray.get is 0, which is a valid
- // position. Use a sentry value to prevent erroneously selecting item 0.
- final int rowKey = mRowBounds.get(row).lowerLimit;
- int position = items.get(rowKey, NOT_SET);
- if (position != NOT_SET) {
- String id = mAdapter.getModelId(position);
- if (id != null) {
- // The adapter inserts items for UI layout purposes that aren't associated
- // with files. Those will have a null model ID. Don't select them.
- if (canSelect(id)) {
- mSelection.add(id);
- }
- }
- if (isPossiblePositionNearestOrigin(column, columnStartIndex, columnEndIndex,
- row, rowStartIndex, rowEndIndex)) {
- // If this is the position nearest the origin, record it now so that it
- // can be returned by endSelection() later.
- mPositionNearestOrigin = position;
- }
- }
- }
- }
- }
-
- /**
- * @return True if the item is selectable.
- */
- private boolean canSelect(String id) {
- // TODO: Simplify the logic, so the check whether we can select is done in one place.
- // Consider injecting FragmentTuner, or move the checks from MultiSelectManager to
- // Selection.
- for (OnSelectionChangedListener listener : mOnSelectionChangedListeners) {
- if (!listener.onBeforeItemStateChange(id, true)) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * @return Returns true if the position is the nearest to the origin, or, in the case of the
- * lower-right corner, whether it is possible that the position is the nearest to the
- * origin. See comment below for reasoning for this special case.
- */
- private boolean isPossiblePositionNearestOrigin(int columnIndex, int columnStartIndex,
- int columnEndIndex, int rowIndex, int rowStartIndex, int rowEndIndex) {
- int corner = computeCornerNearestOrigin();
- switch (corner) {
- case UPPER_LEFT:
- return columnIndex == columnStartIndex && rowIndex == rowStartIndex;
- case UPPER_RIGHT:
- return columnIndex == columnEndIndex && rowIndex == rowStartIndex;
- case LOWER_LEFT:
- return columnIndex == columnStartIndex && rowIndex == rowEndIndex;
- case LOWER_RIGHT:
- // Note that in some cases, the last row will not have as many items as there
- // are columns (e.g., if there are 4 items and 3 columns, the second row will
- // only have one item in the first column). This function is invoked for each
- // position from left to right, so return true for any position in the bottom
- // row and only the right-most position in the bottom row will be recorded.
- return rowIndex == rowEndIndex;
- default:
- throw new RuntimeException("Invalid corner type.");
- }
- }
-
- /**
- * Listener for changes in which items have been band selected.
- */
- static interface OnSelectionChangedListener {
- public void onSelectionChanged(Set<String> updatedSelection);
- public boolean onBeforeItemStateChange(String id, boolean nextState);
- }
-
- void addOnSelectionChangedListener(OnSelectionChangedListener listener) {
- mOnSelectionChangedListeners.add(listener);
- }
-
- void removeOnSelectionChangedListener(OnSelectionChangedListener listener) {
- mOnSelectionChangedListeners.remove(listener);
- }
-
- /**
- * Limits of a view item. For example, if an item's left side is at x-value 5 and its right side
- * is at x-value 10, the limits would be from 5 to 10. Used to record the left- and right sides
- * of item columns and the top- and bottom sides of item rows so that it can be determined
- * whether the pointer is located within the bounds of an item.
- */
- private static class Limits implements Comparable<Limits> {
- int lowerLimit;
- int upperLimit;
-
- Limits(int lowerLimit, int upperLimit) {
- this.lowerLimit = lowerLimit;
- this.upperLimit = upperLimit;
- }
-
- @Override
- public int compareTo(Limits other) {
- return lowerLimit - other.lowerLimit;
- }
-
- @Override
- public boolean equals(Object other) {
- if (!(other instanceof Limits)) {
- return false;
- }
-
- return ((Limits) other).lowerLimit == lowerLimit &&
- ((Limits) other).upperLimit == upperLimit;
- }
-
- @Override
- public String toString() {
- return "(" + lowerLimit + ", " + upperLimit + ")";
- }
- }
-
- /**
- * The location of a coordinate relative to items. This class represents a general area of the
- * view as it relates to band selection rather than an explicit point. For example, two
- * different points within an item are considered to have the same "location" because band
- * selection originating within the item would select the same items no matter which point
- * was used. Same goes for points between items as well as those at the very beginning or end
- * of the view.
- *
- * Tracking a coordinate (e.g., an x-value) as a CoordinateLocation instead of as an int has the
- * advantage of tying the value to the Limits of items along that axis. This allows easy
- * selection of items within those Limits as opposed to a search through every item to see if a
- * given coordinate value falls within those Limits.
- */
- private static class RelativeCoordinate
- implements Comparable<RelativeCoordinate> {
- /**
- * Location describing points after the last known item.
- */
- static final int AFTER_LAST_ITEM = 0;
-
- /**
- * Location describing points before the first known item.
- */
- static final int BEFORE_FIRST_ITEM = 1;
-
- /**
- * Location describing points between two items.
- */
- static final int BETWEEN_TWO_ITEMS = 2;
-
- /**
- * Location describing points within the limits of one item.
- */
- static final int WITHIN_LIMITS = 3;
-
- /**
- * The type of this coordinate, which is one of AFTER_LAST_ITEM, BEFORE_FIRST_ITEM,
- * BETWEEN_TWO_ITEMS, or WITHIN_LIMITS.
- */
- final int type;
-
- /**
- * The limits before the coordinate; only populated when type == WITHIN_LIMITS or type ==
- * BETWEEN_TWO_ITEMS.
- */
- Limits limitsBeforeCoordinate;
-
- /**
- * The limits after the coordinate; only populated when type == BETWEEN_TWO_ITEMS.
- */
- Limits limitsAfterCoordinate;
-
- // Limits of the first known item; only populated when type == BEFORE_FIRST_ITEM.
- Limits mFirstKnownItem;
- // Limits of the last known item; only populated when type == AFTER_LAST_ITEM.
- Limits mLastKnownItem;
-
- /**
- * @param limitsList The sorted limits list for the coordinate type. If this
- * CoordinateLocation is an x-value, mXLimitsList should be passed; otherwise,
- * mYLimitsList should be pased.
- * @param value The coordinate value.
- */
- RelativeCoordinate(List<Limits> limitsList, int value) {
- int index = Collections.binarySearch(limitsList, new Limits(value, value));
-
- if (index >= 0) {
- this.type = WITHIN_LIMITS;
- this.limitsBeforeCoordinate = limitsList.get(index);
- } else if (~index == 0) {
- this.type = BEFORE_FIRST_ITEM;
- this.mFirstKnownItem = limitsList.get(0);
- } else if (~index == limitsList.size()) {
- Limits lastLimits = limitsList.get(limitsList.size() - 1);
- if (lastLimits.lowerLimit <= value && value <= lastLimits.upperLimit) {
- this.type = WITHIN_LIMITS;
- this.limitsBeforeCoordinate = lastLimits;
- } else {
- this.type = AFTER_LAST_ITEM;
- this.mLastKnownItem = lastLimits;
- }
- } else {
- Limits limitsBeforeIndex = limitsList.get(~index - 1);
- if (limitsBeforeIndex.lowerLimit <= value && value <= limitsBeforeIndex.upperLimit) {
- this.type = WITHIN_LIMITS;
- this.limitsBeforeCoordinate = limitsList.get(~index - 1);
- } else {
- this.type = BETWEEN_TWO_ITEMS;
- this.limitsBeforeCoordinate = limitsList.get(~index - 1);
- this.limitsAfterCoordinate = limitsList.get(~index);
- }
- }
- }
-
- int toComparisonValue() {
- if (type == BEFORE_FIRST_ITEM) {
- return mFirstKnownItem.lowerLimit - 1;
- } else if (type == AFTER_LAST_ITEM) {
- return mLastKnownItem.upperLimit + 1;
- } else if (type == BETWEEN_TWO_ITEMS) {
- return limitsBeforeCoordinate.upperLimit + 1;
- } else {
- return limitsBeforeCoordinate.lowerLimit;
- }
- }
-
- @Override
- public boolean equals(Object other) {
- if (!(other instanceof RelativeCoordinate)) {
- return false;
- }
-
- RelativeCoordinate otherCoordinate = (RelativeCoordinate) other;
- return toComparisonValue() == otherCoordinate.toComparisonValue();
- }
-
- @Override
- public int compareTo(RelativeCoordinate other) {
- return toComparisonValue() - other.toComparisonValue();
- }
- }
-
- /**
- * The location of a point relative to the Limits of nearby items; consists of both an x- and
- * y-RelativeCoordinateLocation.
- */
- private class RelativePoint {
- final RelativeCoordinate xLocation;
- final RelativeCoordinate yLocation;
-
- RelativePoint(Point point) {
- this.xLocation = new RelativeCoordinate(mColumnBounds, point.x);
- this.yLocation = new RelativeCoordinate(mRowBounds, point.y);
- }
-
- @Override
- public boolean equals(Object other) {
- if (!(other instanceof RelativePoint)) {
- return false;
- }
-
- RelativePoint otherPoint = (RelativePoint) other;
- return xLocation.equals(otherPoint.xLocation) && yLocation.equals(otherPoint.yLocation);
- }
- }
-
- /**
- * Generates a rectangle which contains the items selected by the pointer and origin.
- * @return The rectangle, or null if no items were selected.
- */
- private Rect computeBounds() {
- Rect rect = new Rect();
- rect.left = getCoordinateValue(
- min(mRelativeOrigin.xLocation, mRelativePointer.xLocation),
- mColumnBounds,
- true);
- rect.right = getCoordinateValue(
- max(mRelativeOrigin.xLocation, mRelativePointer.xLocation),
- mColumnBounds,
- false);
- rect.top = getCoordinateValue(
- min(mRelativeOrigin.yLocation, mRelativePointer.yLocation),
- mRowBounds,
- true);
- rect.bottom = getCoordinateValue(
- max(mRelativeOrigin.yLocation, mRelativePointer.yLocation),
- mRowBounds,
- false);
- return rect;
- }
-
- /**
- * Computes the corner of the selection nearest the origin.
- * @return
- */
- private int computeCornerNearestOrigin() {
- int cornerValue = 0;
-
- if (mRelativeOrigin.yLocation ==
- min(mRelativeOrigin.yLocation, mRelativePointer.yLocation)) {
- cornerValue |= UPPER;
- } else {
- cornerValue |= LOWER;
- }
-
- if (mRelativeOrigin.xLocation ==
- min(mRelativeOrigin.xLocation, mRelativePointer.xLocation)) {
- cornerValue |= LEFT;
- } else {
- cornerValue |= RIGHT;
- }
-
- return cornerValue;
- }
-
- private RelativeCoordinate min(RelativeCoordinate first, RelativeCoordinate second) {
- return first.compareTo(second) < 0 ? first : second;
- }
-
- private RelativeCoordinate max(RelativeCoordinate first, RelativeCoordinate second) {
- return first.compareTo(second) > 0 ? first : second;
- }
-
- /**
- * @return The absolute coordinate (i.e., the x- or y-value) of the given relative
- * coordinate.
- */
- private int getCoordinateValue(RelativeCoordinate coordinate,
- List<Limits> limitsList, boolean isStartOfRange) {
- switch (coordinate.type) {
- case RelativeCoordinate.BEFORE_FIRST_ITEM:
- return limitsList.get(0).lowerLimit;
- case RelativeCoordinate.AFTER_LAST_ITEM:
- return limitsList.get(limitsList.size() - 1).upperLimit;
- case RelativeCoordinate.BETWEEN_TWO_ITEMS:
- if (isStartOfRange) {
- return coordinate.limitsAfterCoordinate.lowerLimit;
- } else {
- return coordinate.limitsBeforeCoordinate.upperLimit;
- }
- case RelativeCoordinate.WITHIN_LIMITS:
- return coordinate.limitsBeforeCoordinate.lowerLimit;
- }
-
- throw new RuntimeException("Invalid coordinate value.");
- }
-
- private boolean areItemsCoveredByBand(
- RelativePoint first, RelativePoint second) {
- return doesCoordinateLocationCoverItems(first.xLocation, second.xLocation) &&
- doesCoordinateLocationCoverItems(first.yLocation, second.yLocation);
- }
-
- private boolean doesCoordinateLocationCoverItems(
- RelativeCoordinate pointerCoordinate,
- RelativeCoordinate originCoordinate) {
- if (pointerCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM &&
- originCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM) {
- return false;
- }
-
- if (pointerCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM &&
- originCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM) {
- return false;
- }
-
- if (pointerCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS &&
- originCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS &&
- pointerCoordinate.limitsBeforeCoordinate.equals(
- originCoordinate.limitsBeforeCoordinate) &&
- pointerCoordinate.limitsAfterCoordinate.equals(
- originCoordinate.limitsAfterCoordinate)) {
- return false;
- }
-
- return true;
- }
- }
}
diff --git a/src/com/android/documentsui/services/Job.java b/src/com/android/documentsui/services/Job.java
index b4f1299..b8f8fba 100644
--- a/src/com/android/documentsui/services/Job.java
+++ b/src/com/android/documentsui/services/Job.java
@@ -308,7 +308,9 @@
String id, DocumentStack stack, List<DocumentInfo> srcs,
DocumentInfo srcParent) {
assert(!srcs.isEmpty());
- assert(stack.peek().isDirectory()); // we can't currently delete from archives.
+ // stack is empty if we delete docs from recent.
+ // we can't currently delete from archives.
+ assert(stack.isEmpty() || stack.peek().isDirectory());
return new DeleteJob(service, appContext, listener, id, stack, srcs, srcParent);
}
}
diff --git a/tests/src/com/android/documentsui/dirlist/MultiSelectManager_GridModelTest.java b/tests/src/com/android/documentsui/dirlist/BandController_GridModelTest.java
similarity index 97%
rename from tests/src/com/android/documentsui/dirlist/MultiSelectManager_GridModelTest.java
rename to tests/src/com/android/documentsui/dirlist/BandController_GridModelTest.java
index e401de1..59547ad 100644
--- a/tests/src/com/android/documentsui/dirlist/MultiSelectManager_GridModelTest.java
+++ b/tests/src/com/android/documentsui/dirlist/BandController_GridModelTest.java
@@ -16,7 +16,7 @@
package com.android.documentsui.dirlist;
-import static com.android.documentsui.dirlist.MultiSelectManager.GridModel.NOT_SET;
+import static com.android.documentsui.dirlist.BandController.GridModel.NOT_SET;
import android.graphics.Point;
import android.graphics.Rect;
@@ -24,14 +24,14 @@
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
-import com.android.documentsui.dirlist.MultiSelectManager.GridModel;
+import com.android.documentsui.dirlist.BandController.GridModel;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@SmallTest
-public class MultiSelectManager_GridModelTest extends AndroidTestCase {
+public class BandController_GridModelTest extends AndroidTestCase {
private static final int VIEW_PADDING_PX = 5;
private static final int CHILD_VIEW_EDGE_PX = 100;
@@ -279,7 +279,7 @@
model.onScrolled(null, 0, dy);
}
- private static final class TestEnvironment implements MultiSelectManager.SelectionEnvironment {
+ private static final class TestEnvironment implements BandController.SelectionEnvironment {
private final int mNumColumns;
private final int mNumRows;
diff --git a/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java b/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java
index 9447d9c..9401da8 100644
--- a/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java
+++ b/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java
@@ -23,6 +23,7 @@
import com.android.documentsui.TestInputEvent;
import com.android.documentsui.dirlist.MultiSelectManager.Selection;
+
import com.google.common.collect.Lists;
import java.util.ArrayList;
@@ -43,14 +44,12 @@
private MultiSelectManager mManager;
private TestCallback mCallback;
- private TestSelectionEnvironment mEnv;
private TestDocumentsAdapter mAdapter;
public void setUp() throws Exception {
mCallback = new TestCallback();
- mEnv = new TestSelectionEnvironment(items);
mAdapter = new TestDocumentsAdapter(items);
- mManager = new MultiSelectManager(mEnv, mAdapter, MultiSelectManager.MODE_MULTIPLE, null);
+ mManager = new MultiSelectManager(mAdapter, MultiSelectManager.MODE_MULTIPLE, null);
mManager.addCallback(mCallback);
}
@@ -174,7 +173,7 @@
}
public void testSingleSelectMode() {
- mManager = new MultiSelectManager(mEnv, mAdapter, MultiSelectManager.MODE_SINGLE, null);
+ mManager = new MultiSelectManager(mAdapter, MultiSelectManager.MODE_SINGLE, null);
mManager.addCallback(mCallback);
longPress(20);
tap(13);
@@ -182,7 +181,7 @@
}
public void testSingleSelectMode_ShiftTap() {
- mManager = new MultiSelectManager(mEnv, mAdapter, MultiSelectManager.MODE_SINGLE, null);
+ mManager = new MultiSelectManager(mAdapter, MultiSelectManager.MODE_SINGLE, null);
mManager.addCallback(mCallback);
longPress(13);
shiftTap(20);
@@ -229,7 +228,7 @@
}
public void testRangeSelection_singleSelect() {
- mManager = new MultiSelectManager(mEnv, mAdapter, MultiSelectManager.MODE_SINGLE, null);
+ mManager = new MultiSelectManager(mAdapter, MultiSelectManager.MODE_SINGLE, null);
mManager.addCallback(mCallback);
mManager.startRangeSelection(11);
mManager.snapRangeSelection(19);
diff --git a/tests/src/com/android/documentsui/dirlist/TestSelectionEnvironment.java b/tests/src/com/android/documentsui/dirlist/TestSelectionEnvironment.java
index f564769..b69787c 100644
--- a/tests/src/com/android/documentsui/dirlist/TestSelectionEnvironment.java
+++ b/tests/src/com/android/documentsui/dirlist/TestSelectionEnvironment.java
@@ -20,7 +20,7 @@
import android.graphics.Rect;
import android.support.v7.widget.RecyclerView.OnScrollListener;
-import com.android.documentsui.dirlist.MultiSelectManager.SelectionEnvironment;
+import com.android.documentsui.dirlist.BandController.SelectionEnvironment;
import java.util.List;