diff options
author | 2017-08-21 12:27:10 -0700 | |
---|---|---|
committer | 2017-08-22 09:40:55 -0700 | |
commit | 53c8e803529c01faf6cefe60b25f75cc65f0537d (patch) | |
tree | c5640fab691344151d7e011e064e5607f74c6f92 | |
parent | c5421cd37896d044b650aab5d837b0e21dde2755 (diff) |
Extract SelectionManager interface.
This will be necessary to decouple selection manager from DocumentsUI.
Why? In order eliminate the "reset" method that DocumentsUI depends on
to accommodate it's fragment transaction model...without incuring
further DocumentsUI refactoring costs.
Eliminate refs to documetnsui.Shared.
Update tests to not use AndroidTestCase.
Change-Id: I56cc85c3659e2116203f829c6f0d46b32a6d3cb6
Bug: 64847011
Test: Small and medium passing.
23 files changed, 766 insertions, 545 deletions
diff --git a/src/com/android/documentsui/FocusManager.java b/src/com/android/documentsui/FocusManager.java index 8eebe34c2..dfedcd912 100644 --- a/src/com/android/documentsui/FocusManager.java +++ b/src/com/android/documentsui/FocusManager.java @@ -47,8 +47,8 @@ import com.android.documentsui.base.Procedure; import com.android.documentsui.dirlist.DocumentHolder; import com.android.documentsui.dirlist.DocumentsAdapter; import com.android.documentsui.dirlist.FocusHandler; -import com.android.documentsui.Model.Update; import com.android.documentsui.selection.SelectionManager; +import com.android.documentsui.Model.Update; import java.util.ArrayList; import java.util.List; diff --git a/src/com/android/documentsui/Injector.java b/src/com/android/documentsui/Injector.java index f028f0efc..4e96aa94c 100644 --- a/src/com/android/documentsui/Injector.java +++ b/src/com/android/documentsui/Injector.java @@ -32,7 +32,6 @@ import com.android.documentsui.dirlist.DocumentsAdapter; import com.android.documentsui.prefs.ScopedPreferences; import com.android.documentsui.queries.SearchViewManager; import com.android.documentsui.selection.SelectionManager; -import com.android.documentsui.selection.SelectionManager.SelectionPredicate; import com.android.documentsui.ui.DialogController; import com.android.documentsui.ui.MessageBuilder; import com.android.internal.annotations.VisibleForTesting; @@ -120,7 +119,7 @@ public class Injector<T extends ActionHandler> { } public SelectionManager getSelectionManager( - DocumentsAdapter adapter, SelectionPredicate canSetState) { + DocumentsAdapter adapter, SelectionManager.SelectionPredicate canSetState) { return selectionMgr.reset(adapter, adapter, canSetState); } diff --git a/src/com/android/documentsui/RootsMonitor.java b/src/com/android/documentsui/RootsMonitor.java index 82b3781cf..4e0af3b06 100644 --- a/src/com/android/documentsui/RootsMonitor.java +++ b/src/com/android/documentsui/RootsMonitor.java @@ -32,7 +32,7 @@ import com.android.documentsui.base.State; import com.android.documentsui.dirlist.AnimationView; import com.android.documentsui.queries.SearchViewManager; import com.android.documentsui.roots.ProvidersAccess; -import com.android.documentsui.selection.SelectionManager; +import com.android.documentsui.selection.DefaultSelectionManager; import java.util.Collection; diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java index 562968513..47ea8f7df 100644 --- a/src/com/android/documentsui/dirlist/DirectoryFragment.java +++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java @@ -94,6 +94,7 @@ 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.SelectionManager.SelectionPredicate; import com.android.documentsui.services.FileOperation; import com.android.documentsui.services.FileOperationService; import com.android.documentsui.services.FileOperationService.OpType; @@ -315,7 +316,8 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On mModel.addUpdateListener(mAdapter.getModelUpdateListener()); mModel.addUpdateListener(mModelUpdateListener); - mSelectionMgr = mInjector.getSelectionManager(mAdapter, this::canSetSelectionState); + SelectionManager.SelectionPredicate canSelect = this::canSetSelectionState; + mSelectionMgr = mInjector.getSelectionManager(mAdapter, canSelect); mFocusManager = mInjector.getFocusManager(mRecView, mModel); mActions = mInjector.getActionHandler(mReloadLock); @@ -332,6 +334,7 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On mRecView, mAdapter, mSelectionMgr, + canSelect, mReloadLock, (int pos) -> { // The band selection model only operates on documents and directories. diff --git a/src/com/android/documentsui/files/FilesActivity.java b/src/com/android/documentsui/files/FilesActivity.java index 8dc24a9be..da8c6d179 100644 --- a/src/com/android/documentsui/files/FilesActivity.java +++ b/src/com/android/documentsui/files/FilesActivity.java @@ -57,6 +57,7 @@ import com.android.documentsui.clipping.DocumentClipper; import com.android.documentsui.dirlist.AnimationView.AnimationType; import com.android.documentsui.dirlist.DirectoryFragment; import com.android.documentsui.prefs.ScopedPreferences; +import com.android.documentsui.selection.DefaultSelectionManager; import com.android.documentsui.selection.SelectionManager; import com.android.documentsui.services.FileOperationService; import com.android.documentsui.sidebar.RootsFragment; @@ -105,7 +106,7 @@ public class FilesActivity extends BaseActivity implements ActionHandler.Addons super.onCreate(icicle); DocumentClipper clipper = DocumentsApplication.getDocumentClipper(this); - mInjector.selectionMgr = new SelectionManager(SelectionManager.MODE_MULTIPLE); + mInjector.selectionMgr = new DefaultSelectionManager(SelectionManager.MODE_MULTIPLE); mInjector.focusManager = new FocusManager( mInjector.features, diff --git a/src/com/android/documentsui/picker/PickActivity.java b/src/com/android/documentsui/picker/PickActivity.java index fb43ce6ee..deb3ebb90 100644 --- a/src/com/android/documentsui/picker/PickActivity.java +++ b/src/com/android/documentsui/picker/PickActivity.java @@ -49,6 +49,7 @@ import com.android.documentsui.base.Shared; import com.android.documentsui.base.State; import com.android.documentsui.dirlist.DirectoryFragment; import com.android.documentsui.prefs.ScopedPreferences; +import com.android.documentsui.selection.DefaultSelectionManager; import com.android.documentsui.selection.SelectionManager; import com.android.documentsui.services.FileOperationService; import com.android.documentsui.sidebar.RootsFragment; @@ -94,7 +95,7 @@ public class PickActivity extends BaseActivity implements ActionHandler.Addons { super.onCreate(icicle); - mInjector.selectionMgr = new SelectionManager( + mInjector.selectionMgr = new DefaultSelectionManager( mState.allowMultiple ? SelectionManager.MODE_MULTIPLE : SelectionManager.MODE_SINGLE); diff --git a/src/com/android/documentsui/selection/BandController.java b/src/com/android/documentsui/selection/BandController.java index d9afeaf85..bfe960b54 100644 --- a/src/com/android/documentsui/selection/BandController.java +++ b/src/com/android/documentsui/selection/BandController.java @@ -16,7 +16,6 @@ package com.android.documentsui.selection; -import static com.android.documentsui.base.Shared.DEBUG; import static com.android.documentsui.ui.ViewAutoScroller.NOT_SET; import android.graphics.Point; @@ -37,6 +36,7 @@ import com.android.documentsui.DirectoryReloadLock; import com.android.documentsui.R; import com.android.documentsui.base.Events.InputEvent; import com.android.documentsui.dirlist.DocumentsAdapter; +import com.android.documentsui.selection.SelectionManager.SelectionPredicate; import com.android.documentsui.ui.ViewAutoScroller; import com.android.documentsui.ui.ViewAutoScroller.ScrollActionDelegate; import com.android.documentsui.ui.ViewAutoScroller.ScrollDistanceDelegate; @@ -45,7 +45,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.function.IntPredicate; @@ -56,12 +55,13 @@ import java.util.function.IntPredicate; */ public class BandController extends OnScrollListener { + private static final boolean DEBUG = false; private static final String TAG = "BandController"; private final Runnable mModelBuilder; private final SelectionEnvironment mEnvironment; private final DocumentsAdapter mAdapter; - private final SelectionManager mSelectionManager; + private final SelectionManager mSelectionMgr; private final DirectoryReloadLock mLock; private final Runnable mViewScroller; private final GridModel.OnSelectionChangedListener mGridListener; @@ -73,15 +73,17 @@ public class BandController extends OnScrollListener { @Nullable private BandController.GridModel mModel; private Selection mSelection; + private SelectionManager.SelectionPredicate mCanSelect; public BandController( final RecyclerView view, DocumentsAdapter adapter, SelectionManager selectionManager, + SelectionManager.SelectionPredicate canSelect, DirectoryReloadLock lock, IntPredicate gridItemTester) { this(new RuntimeSelectionEnvironment(view), adapter, selectionManager, - lock, gridItemTester); + canSelect, lock, gridItemTester); } @VisibleForTesting @@ -89,6 +91,7 @@ public class BandController extends OnScrollListener { SelectionEnvironment env, DocumentsAdapter adapter, SelectionManager selectionManager, + SelectionManager.SelectionPredicate canSelect, DirectoryReloadLock lock, IntPredicate gridItemTester) { @@ -97,7 +100,8 @@ public class BandController extends OnScrollListener { mEnvironment = env; mAdapter = adapter; - mSelectionManager = selectionManager; + mSelectionMgr = selectionManager; + mCanSelect = canSelect; mEnvironment.addOnScrollListener(this); mViewScroller = new ViewAutoScroller( @@ -159,12 +163,12 @@ public class BandController extends OnScrollListener { @Override public void onSelectionChanged(Set<String> updatedSelection) { - BandController.this.onSelectionChanged(updatedSelection); + mSelectionMgr.setProvisionalSelection(updatedSelection); } @Override public boolean onBeforeItemStateChange(String id, boolean nextState) { - return BandController.this.onBeforeItemStateChange(id, nextState); + return mCanSelect.test(id, nextState); } }; @@ -189,7 +193,7 @@ public class BandController extends OnScrollListener { public boolean onInterceptTouchEvent(InputEvent e) { if (shouldStart(e)) { if (!e.isCtrlKeyDown()) { - mSelectionManager.clearSelection(); + mSelectionMgr.clearSelection(); } startBandSelect(e.getOrigin()); } else if (shouldStop(e)) { @@ -332,7 +336,7 @@ public class BandController extends OnScrollListener { if (mSelection.contains(mAdapter.getStableId(firstSelected))) { // TODO: firstSelected should really be lastSelected, we want to anchor the item // where the mouse-up occurred. - mSelectionManager.setSelectionRangeBegin(firstSelected); + mSelectionMgr.setSelectionRangeBegin(firstSelected); } else { // TODO: Check if this is really happening. Log.w(TAG, "First selected by band is NOT in selection!"); @@ -344,18 +348,6 @@ public class BandController extends OnScrollListener { mLock.unblock(); } - 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.canSetState(id, nextState); - } - @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (!isActive()) { @@ -429,7 +421,11 @@ public class BandController extends OnScrollListener { // should expand from when Shift+click is used. private int mPositionNearestOrigin = NOT_SET; - GridModel(SelectionEnvironment helper, IntPredicate gridItemTester, DocumentsAdapter adapter) { + GridModel( + SelectionEnvironment helper, + IntPredicate gridItemTester, + DocumentsAdapter adapter) { + mHelper = helper; mAdapter = adapter; mGridItemTester = gridItemTester; diff --git a/src/com/android/documentsui/selection/DefaultSelectionManager.java b/src/com/android/documentsui/selection/DefaultSelectionManager.java new file mode 100644 index 000000000..417c92515 --- /dev/null +++ b/src/com/android/documentsui/selection/DefaultSelectionManager.java @@ -0,0 +1,499 @@ +/* + * 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.selection.Shared.DEBUG; +import static com.android.documentsui.selection.Shared.TAG; + +import android.support.annotation.VisibleForTesting; +import android.support.v7.widget.RecyclerView; +import android.util.Log; + +import java.util.ArrayList; +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 DefaultSelectionManager implements SelectionManager { + + private final Selection mSelection = new Selection(); + + private final List<SelectionManager.EventListener> mEventListeners = new ArrayList<>(1); + + private @Nullable RecyclerView.Adapter<?> mAdapter; + private @Nullable SelectionManager.Environment mIdLookup; + private @Nullable Range mRanger; + private boolean mSingleSelect; + + private RecyclerView.AdapterDataObserver mAdapterObserver; + private SelectionManager.SelectionPredicate mCanSetState; + + public DefaultSelectionManager(@SelectionMode int mode) { + mSingleSelect = mode == MODE_SINGLE; + } + + @Override + public SelectionManager reset( + RecyclerView.Adapter<?> adapter, + SelectionManager.Environment idLookup, + SelectionManager.SelectionPredicate canSetState) { + + mEventListeners.clear(); + if (mAdapter != null && mAdapterObserver != null) { + mAdapter.unregisterAdapterDataObserver(mAdapterObserver); + } + + clearSelectionQuietly(); + + assert adapter != null; + assert idLookup != null; + assert canSetState != null; + + mAdapter = adapter; + mIdLookup = idLookup; + mCanSetState = canSetState; + + mAdapterObserver = new RecyclerView.AdapterDataObserver() { + + private List<String> mModelIds; + + @Override + public void onChanged() { + // Update the selection to remove any disappeared IDs. + mSelection.cancelProvisionalSelection(); + mSelection.intersect(mIdLookup.getStableIds()); + + notifyDataChanged(); + } + + @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; + } + + @Override + public void bindContoller(BandController controller) { + // Provides BandController with access to private mSelection state. + controller.bindSelection(mSelection); + } + + @Override + public void addEventListener(SelectionManager.EventListener callback) { + assert callback != null; + mEventListeners.add(callback); + } + + @Override + public boolean hasSelection() { + return !mSelection.isEmpty(); + } + + @Override + public Selection getSelection() { + return mSelection; + } + + @Override + public Selection getSelection(Selection dest) { + dest.copyFrom(mSelection); + return dest; + } + + @Override + @VisibleForTesting + public void replaceSelection(Iterable<String> ids) { + clearSelection(); + setItemsSelected(ids, true); + } + + @Override + public void restoreSelection(Selection other) { + setItemsSelectedQuietly(other.mSelection, true); + // NOTE: We intentionally don't restore provisional selection. It's provisional. + notifySelectionRestored(); + } + + @Override + 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; + } + + @Override + 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); + } + } + + @Override + public void toggleSelection(String modelId) { + assert modelId != null; + + final boolean changed = mSelection.contains(modelId) + ? attemptDeselect(modelId) + : attemptSelect(modelId); + + if (changed) { + notifySelectionChanged(); + } + } + + @Override + public void startRangeSelection(int pos) { + attemptSelect(mIdLookup.getStableId(pos)); + setSelectionRangeBegin(pos); + } + + @Override + public void snapRangeSelection(int pos) { + snapRangeSelection(pos, RANGE_REGULAR); + } + + @Override + public void snapProvisionalRangeSelection(int pos) { + snapRangeSelection(pos, RANGE_PROVISIONAL); + } + + /* + * Starts and extends range selection in one go. This assumes item at startPos is not selected + * beforehand. + */ + @Override + public void formNewSelectionRange(int startPos, int endPos) { + assert !mSelection.contains(mIdLookup.getStableId(startPos)); + startRangeSelection(startPos); + snapRangeSelection(endPos); + } + + /** + * 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(); + } + + @Override + public void cancelProvisionalSelection() { + for (String id : mSelection.mProvisionalSelection) { + notifyItemStateChanged(id, false); + } + mSelection.cancelProvisionalSelection(); + } + + @Override + public void setProvisionalSelection(Set<String> newSelection) { + Map<String, Boolean> delta = mSelection.setProvisionalSelection(newSelection); + for (Map.Entry<String, Boolean> entry: delta.entrySet()) { + notifyItemStateChanged(entry.getKey(), entry.getValue()); + } + notifySelectionChanged(); + } + + @Override + public void endRangeSelection() { + mRanger = null; + // Clean up in case there was any leftover provisional selection + cancelProvisionalSelection(); + } + + @Override + public boolean isRangeSelectionActive() { + return mRanger != null; + } + + @Override + public void setSelectionRangeBegin(int position) { + if (position == RecyclerView.NO_POSITION) { + return; + } + + if (mSelection.contains(mIdLookup.getStableId(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 there's nothing in the selection and there is an active ranger it results + // in unexpected behavior when the user tries to start range selection: the item + // which the ranger 'thinks' is the already selected anchor becomes unselectable + if (mSelection.isEmpty() && isRangeSelectionActive()) { + endRangeSelection(); + } + 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); + } + + private void notifyDataChanged() { + + notifySelectionReset(); + + final int lastListener = mEventListeners.size() - 1; + for (String id : mSelection) { + // TODO: Why do we deselect in notify changed. + if (!canSetState(id, true)) { + attemptDeselect(id); + } else { + for (int i = lastListener; i >= 0; i--) { + mEventListeners.get(i).onItemStateChanged(id, true); + } + } + } + } + + /** + * 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 = mEventListeners.size() - 1; + for (int i = lastListener; i >= 0; i--) { + mEventListeners.get(i).onItemStateChanged(id, selected); + } + mIdLookup.onSelectionStateChanged(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 = mEventListeners.size() - 1; + for (int i = lastListener; i > -1; i--) { + mEventListeners.get(i).onSelectionChanged(); + } + } + + private void notifySelectionRestored() { + int lastListener = mEventListeners.size() - 1; + for (int i = lastListener; i > -1; i--) { + mEventListeners.get(i).onSelectionRestored(); + } + } + + private void notifySelectionReset() { + int lastListener = mEventListeners.size() - 1; + for (int i = lastListener; i > -1; i--) { + mEventListeners.get(i).onSelectionReset(); + } + } + + 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 = mIdLookup.getStableId(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 = mIdLookup.getStableId(i); + if (id == null) { + continue; + } + + boolean changedState = false; + if (selected) { + boolean canSelect = canSetState(id, true); + if (canSelect && !mSelection.mSelection.contains(id)) { + mSelection.mProvisionalSelection.add(id); + changedState = true; + } + } else { + mSelection.mProvisionalSelection.remove(id); + changedState = true; + } + + // Only notify item callbacks when something's state is actually changed in provisional + // selection. + if (changedState) { + notifyItemStateChanged(id, selected); + } + } + notifySelectionChanged(); + } +} diff --git a/src/com/android/documentsui/selection/GestureSelector.java b/src/com/android/documentsui/selection/GestureSelector.java index 7c5f217ee..92ce0f41b 100644 --- a/src/com/android/documentsui/selection/GestureSelector.java +++ b/src/com/android/documentsui/selection/GestureSelector.java @@ -16,8 +16,6 @@ package com.android.documentsui.selection; -import static com.android.documentsui.base.Shared.DEBUG; - import android.graphics.Point; import android.support.annotation.VisibleForTesting; import android.support.v7.widget.RecyclerView; @@ -39,6 +37,8 @@ import javax.annotation.Nullable; * the interception going if necessary. */ public final class GestureSelector { + + private static final boolean DEBUG = false; private final String TAG = "GestureSelector"; private final SelectionManager mSelectionMgr; @@ -85,6 +85,7 @@ public final class GestureSelector { SelectionManager selectionMgr, RecyclerView scrollView, DirectoryReloadLock lock) { + ScrollActionDelegate actionDelegate = new ScrollActionDelegate() { @Override public void scrollBy(int dy) { diff --git a/src/com/android/documentsui/selection/Range.java b/src/com/android/documentsui/selection/Range.java index c7db84c48..980006e37 100644 --- a/src/com/android/documentsui/selection/Range.java +++ b/src/com/android/documentsui/selection/Range.java @@ -15,8 +15,8 @@ */ package com.android.documentsui.selection; -import static com.android.documentsui.base.Shared.DEBUG; -import static com.android.documentsui.base.Shared.VERBOSE; +import static com.android.documentsui.selection.Shared.DEBUG; +import static com.android.documentsui.selection.Shared.TAG; import android.support.v7.widget.RecyclerView; import android.util.Log; @@ -27,6 +27,7 @@ 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; @@ -34,7 +35,7 @@ final class Range { private int mEnd = UNDEFINED; public Range(Range.RangeUpdater updater, int begin) { - if (DEBUG) Log.d(SelectionManager.TAG, "New Ranger created beginning @ " + begin); + if (DEBUG) Log.d(TAG, "New Ranger created beginning @ " + begin); mUpdater = updater; mBegin = begin; } @@ -72,7 +73,7 @@ final class Range { assert(mBegin != mEnd); if (position == mEnd) { - if (VERBOSE) Log.v(SelectionManager.TAG, "Ignoring no-op revision for range: " + this); + if (DEBUG) Log.v(TAG, "Ignoring no-op revision for range: " + this); } if (mEnd > mBegin) { diff --git a/src/com/android/documentsui/selection/Selection.java b/src/com/android/documentsui/selection/Selection.java index 8775c5831..4f405358c 100644 --- a/src/com/android/documentsui/selection/Selection.java +++ b/src/com/android/documentsui/selection/Selection.java @@ -101,7 +101,8 @@ public final class Selection implements Iterable<String>, Parcelable { * one (if it exists) is abandoned. * @return Map of ids added or removed. Added ids have a value of true, removed are false. */ - @VisibleForTesting + // TODO: Make this private to selectionmanager. Even other selection classes like BandController + // should not be reaching in and modify selection state. protected Map<String, Boolean> setProvisionalSelection(Set<String> newSelection) { Map<String, Boolean> delta = new HashMap<>(); diff --git a/src/com/android/documentsui/selection/SelectionManager.java b/src/com/android/documentsui/selection/SelectionManager.java index 3664e33b0..5e003b0e4 100644 --- a/src/com/android/documentsui/selection/SelectionManager.java +++ b/src/com/android/documentsui/selection/SelectionManager.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 The Android Open Source Project + * Copyright (C) 2017 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. @@ -16,62 +16,36 @@ package com.android.documentsui.selection; -import static com.android.documentsui.base.Shared.DEBUG; - -import android.annotation.IntDef; -import android.support.annotation.VisibleForTesting; +import android.support.annotation.IntDef; 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.List; - -import javax.annotation.Nullable; +import java.util.Set; /** - * 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 { +public interface SelectionManager { + int MODE_MULTIPLE = 0; + int MODE_SINGLE = 1; @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; + int RANGE_REGULAR = 0; + int RANGE_PROVISIONAL = 1; @IntDef({ - RANGE_REGULAR, - RANGE_PROVISIONAL + 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<EventListener> mEventListeners = new ArrayList<>(1); - - private @Nullable RecyclerView.Adapter<?> mAdapter; - private @Nullable Environment mIdLookup; - private @Nullable Range mRanger; - private boolean mSingleSelect; - - private RecyclerView.AdapterDataObserver mAdapterObserver; - private SelectionPredicate mCanSetState; - - public SelectionManager(@SelectionMode int mode) { - mSingleSelect = mode == MODE_SINGLE; - } /** * Reset allows fragment state to be utilized in the (re-)initialization of the @@ -92,74 +66,10 @@ public final class SelectionManager { * @param canSetState * @return */ - public SelectionManager reset( + SelectionManager reset( RecyclerView.Adapter<?> adapter, - Environment idLookup, - SelectionPredicate canSetState) { - - mEventListeners.clear(); - if (mAdapter != null && mAdapterObserver != null) { - mAdapter.unregisterAdapterDataObserver(mAdapterObserver); - } - - clearSelectionQuietly(); - - assert adapter != null; - assert idLookup != null; - assert canSetState != null; - - mAdapter = adapter; - mIdLookup = idLookup; - mCanSetState = canSetState; - - mAdapterObserver = new RecyclerView.AdapterDataObserver() { - - private List<String> mModelIds; - - @Override - public void onChanged() { - // Update the selection to remove any disappeared IDs. - mSelection.cancelProvisionalSelection(); - mSelection.intersect(mIdLookup.getStableIds()); - - notifyDataChanged(); - } - - @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); - } + SelectionManager.Environment idLookup, + SelectionManager.SelectionPredicate canSetState); /** * Adds {@code callback} such that it will be notified when {@code MultiSelectManager} @@ -167,18 +77,12 @@ public final class SelectionManager { * * @param callback */ - public void addEventListener(EventListener callback) { - assert callback != null; - mEventListeners.add(callback); - } + void addEventListener(SelectionManager.EventListener callback); - public boolean hasSelection() { - return !mSelection.isEmpty(); - } + boolean hasSelection(); /** - * Returns a Selection object that provides a live view - * on the current selection. + * 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 @@ -186,9 +90,7 @@ public final class SelectionManager { * * @return The current selection. */ - public Selection getSelection() { - return mSelection; - } + Selection getSelection(); /** * Updates {@code dest} to reflect the current selection. @@ -196,26 +98,15 @@ public final class SelectionManager { * * @return The Selection instance passed in, for convenience. */ - public Selection getSelection(Selection dest) { - dest.copyFrom(mSelection); - return dest; - } + Selection getSelection(Selection dest); - @VisibleForTesting - public void replaceSelection(Iterable<String> ids) { - clearSelection(); - setItemsSelected(ids, true); - } + void replaceSelection(Iterable<String> ids); /** * 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(); - } + void restoreSelection(Selection other); /** * Sets the selected state of the specified items. Note that the callback will NOT @@ -225,77 +116,19 @@ public final class SelectionManager { * @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; - } + boolean setItemsSelected(Iterable<String> ids, boolean selected); /** * 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); - } - } + void clearSelection(); /** * 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(); - } - } + void toggleSelection(String modelId); /** * Starts a range selection. If a range selection is already active, this will start a new range @@ -303,273 +136,61 @@ public final class SelectionManager { * * @param pos The anchor position for the selection range. */ - public void startRangeSelection(int pos) { - attemptSelect(mIdLookup.getStableId(pos)); - setSelectionRangeBegin(pos); - } + void startRangeSelection(int pos); - public void snapRangeSelection(int pos) { - snapRangeSelection(pos, RANGE_REGULAR); - } - - void snapProvisionalRangeSelection(int pos) { - snapRangeSelection(pos, RANGE_PROVISIONAL); - } + void snapRangeSelection(int pos); /* * Starts and extends range selection in one go. This assumes item at startPos is not selected * beforehand. */ - public void formNewSelectionRange(int startPos, int endPos) { - assert !mSelection.contains(mIdLookup.getStableId(startPos)); - startRangeSelection(startPos); - snapRangeSelection(endPos); - } - - /** - * 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(); - } - - void cancelProvisionalSelection() { - for (String id : mSelection.mProvisionalSelection) { - notifyItemStateChanged(id, false); - } - mSelection.cancelProvisionalSelection(); - } + void formNewSelectionRange(int startPos, int endPos); /** * 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 - cancelProvisionalSelection(); - } + void endRangeSelection(); /** * @return Whether or not there is a current range selection active. */ - public boolean isRangeSelectionActive() { - return mRanger != null; - } + boolean isRangeSelectionActive(); /** * 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(mIdLookup.getStableId(position))) { - mRanger = new Range(this::updateForRange, position); - } - } + void setSelectionRangeBegin(int position); /** - * @param modelId - * @return True if the update was applied. + * @param newSelection */ - private boolean selectAndNotify(String modelId) { - boolean changed = mSelection.add(modelId); - if (changed) { - notifyItemStateChanged(modelId, true); - } - return changed; - } + void setProvisionalSelection(Set<String> newSelection); /** - * @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 there's nothing in the selection and there is an active ranger it results - // in unexpected behavior when the user tries to start range selection: the item - // which the ranger 'thinks' is the already selected anchor becomes unselectable - if (mSelection.isEmpty() && isRangeSelectionActive()) { - endRangeSelection(); - } - if (DEBUG) Log.d(TAG, "Selection after deselect: " + mSelection); - return true; - } else { - if (DEBUG) Log.d(TAG, "Select cancelled by listener."); - return false; - } - } + void cancelProvisionalSelection(); /** - * @param id - * @return True if the update was applied. + * @param pos */ - 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); - } - - private void notifyDataChanged() { - - notifySelectionReset(); - - final int lastListener = mEventListeners.size() - 1; - for (String id : mSelection) { - // TODO: Why do we deselect in notify changed. - if (!canSetState(id, true)) { - attemptDeselect(id); - } else { - for (int i = lastListener; i >= 0; i--) { - mEventListeners.get(i).onItemStateChanged(id, true); - } - } - } - } + // TODO: This is smelly. Maybe this type of logic needs to move into range selection, + // then selection manager can have a startProvisionalRange and startRange. Or + // maybe ranges always start life as provisional. + void snapProvisionalRangeSelection(int pos); /** - * Notifies registered listeners when the selection status of a single item - * (identified by {@code position}) changes. + * Binds band controller to this selection manager, endowing it with special + * powers (like control of provisional selection). + * @param controller */ - void notifyItemStateChanged(String id, boolean selected) { - assert id != null; - int lastListener = mEventListeners.size() - 1; - for (int i = lastListener; i >= 0; i--) { - mEventListeners.get(i).onItemStateChanged(id, selected); - } - mIdLookup.onSelectionStateChanged(id); - } + void bindContoller(BandController controller); - /** - * 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 = mEventListeners.size() - 1; - for (int i = lastListener; i > -1; i--) { - mEventListeners.get(i).onSelectionChanged(); - } - } - - private void notifySelectionRestored() { - int lastListener = mEventListeners.size() - 1; - for (int i = lastListener; i > -1; i--) { - mEventListeners.get(i).onSelectionRestored(); - } - } - - private void notifySelectionReset() { - int lastListener = mEventListeners.size() - 1; - for (int i = lastListener; i > -1; i--) { - mEventListeners.get(i).onSelectionReset(); - } - } - - 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 = mIdLookup.getStableId(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 = mIdLookup.getStableId(i); - if (id == null) { - continue; - } - - boolean changedState = false; - if (selected) { - boolean canSelect = canSetState(id, true); - if (canSelect && !mSelection.mSelection.contains(id)) { - mSelection.mProvisionalSelection.add(id); - changedState = true; - } - } else { - mSelection.mProvisionalSelection.remove(id); - changedState = true; - } - - // Only notify item callbacks when something's state is actually changed in provisional - // selection. - if (changedState) { - notifyItemStateChanged(id, selected); - } - } - notifySelectionChanged(); - } - - public interface EventListener { + interface EventListener { /** * Called when state of an item has been changed. @@ -592,15 +213,10 @@ public final class SelectionManager { void onSelectionRestored(); } - @FunctionalInterface - public interface SelectionPredicate { - boolean test(String id, boolean nextState); - } - /** * Facilitates the use of stable ids. */ - public interface Environment { + interface Environment { /** * @return The model ID of the item at the given adapter position. @@ -619,4 +235,8 @@ public final class SelectionManager { */ void onSelectionStateChanged(String id); } + + interface SelectionPredicate { + boolean test(String id, boolean nextState); + } } diff --git a/src/com/android/documentsui/selection/Shared.java b/src/com/android/documentsui/selection/Shared.java new file mode 100644 index 000000000..b575be1b7 --- /dev/null +++ b/src/com/android/documentsui/selection/Shared.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2017 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; + +final class Shared { + + private Shared() {} + + static final String TAG = "SelectionManager"; + static final boolean DEBUG = false; + static final boolean VERBOSE = false; +} diff --git a/tests/common/com/android/documentsui/selection/SelectionProbe.java b/tests/common/com/android/documentsui/selection/SelectionProbe.java index c38fc3eb9..14aad86d2 100644 --- a/tests/common/com/android/documentsui/selection/SelectionProbe.java +++ b/tests/common/com/android/documentsui/selection/SelectionProbe.java @@ -20,13 +20,13 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import com.android.documentsui.selection.SelectionManager; +import com.android.documentsui.selection.DefaultSelectionManager; import com.android.documentsui.selection.Selection; /** - * Helper class for making assertions against the state of a {@link SelectionManager} instance and - * the consistency of states between {@link SelectionManager} and - * {@link SelectionManager.ItemEventCallback}. + * Helper class for making assertions against the state of a {@link DefaultSelectionManager} instance and + * the consistency of states between {@link DefaultSelectionManager} and + * {@link DefaultSelectionManager.ItemEventCallback}. */ public final class SelectionProbe { diff --git a/tests/common/com/android/documentsui/selection/TestSelectionEventListener.java b/tests/common/com/android/documentsui/selection/TestSelectionEventListener.java index 24f75c64a..9bc1625c7 100644 --- a/tests/common/com/android/documentsui/selection/TestSelectionEventListener.java +++ b/tests/common/com/android/documentsui/selection/TestSelectionEventListener.java @@ -48,6 +48,7 @@ public class TestSelectionEventListener implements SelectionManager.EventListene @Override public void onSelectionReset() { + mSelectionReset = true; mSelected.clear(); } diff --git a/tests/common/com/android/documentsui/testing/SelectionManagers.java b/tests/common/com/android/documentsui/testing/SelectionManagers.java index e9b53586e..f6d4dbe03 100644 --- a/tests/common/com/android/documentsui/testing/SelectionManagers.java +++ b/tests/common/com/android/documentsui/testing/SelectionManagers.java @@ -18,35 +18,40 @@ package com.android.documentsui.testing; import com.android.documentsui.dirlist.DocumentsAdapter; import com.android.documentsui.dirlist.TestDocumentsAdapter; +import com.android.documentsui.selection.DefaultSelectionManager; import com.android.documentsui.selection.SelectionManager; import com.android.documentsui.selection.SelectionManager.SelectionMode; -import com.android.documentsui.selection.SelectionManager.SelectionPredicate; import java.util.Collections; import java.util.List; public class SelectionManagers { + + public static final SelectionManager.SelectionPredicate CAN_SELECT_ANYTHING = new SelectionManager.SelectionPredicate() { + @Override + public boolean test(String id, boolean nextState) { + return true; + } + }; + private SelectionManagers() {} - public static SelectionManager createTestInstance() { + public static DefaultSelectionManager createTestInstance() { return createTestInstance(Collections.emptyList()); } - public static SelectionManager createTestInstance(List<String> docs) { + public static DefaultSelectionManager createTestInstance(List<String> docs) { return createTestInstance(docs, SelectionManager.MODE_MULTIPLE); } - public static SelectionManager createTestInstance( + public static DefaultSelectionManager createTestInstance( List<String> docs, @SelectionMode int mode) { - return createTestInstance( - new TestDocumentsAdapter(docs), - mode, - (String id, boolean nextState) -> true); + return createTestInstance(new TestDocumentsAdapter(docs), mode, CAN_SELECT_ANYTHING); } - public static SelectionManager createTestInstance( - DocumentsAdapter adapter, @SelectionMode int mode, SelectionPredicate canSetState) { - SelectionManager manager = new SelectionManager(mode); + public static DefaultSelectionManager createTestInstance( + DocumentsAdapter adapter, @SelectionMode int mode, SelectionManager.SelectionPredicate canSetState) { + DefaultSelectionManager manager = new DefaultSelectionManager(mode); manager.reset(adapter, adapter, canSetState); return manager; diff --git a/tests/unit/com/android/documentsui/FocusManagerTest.java b/tests/unit/com/android/documentsui/FocusManagerTest.java index d4d76489a..9c5796225 100644 --- a/tests/unit/com/android/documentsui/FocusManagerTest.java +++ b/tests/unit/com/android/documentsui/FocusManagerTest.java @@ -21,8 +21,8 @@ import android.test.suitebuilder.annotation.SmallTest; import com.android.documentsui.base.Features; import com.android.documentsui.dirlist.TestData; -import com.android.documentsui.testing.TestModel; import com.android.documentsui.selection.SelectionManager; +import com.android.documentsui.testing.TestModel; import com.android.documentsui.testing.SelectionManagers; import com.android.documentsui.testing.TestFeatures; import com.android.documentsui.testing.TestRecyclerView; diff --git a/tests/unit/com/android/documentsui/dirlist/DragStartListenerTest.java b/tests/unit/com/android/documentsui/dirlist/DragStartListenerTest.java index 6cfdd9567..9abf4b2a9 100644 --- a/tests/unit/com/android/documentsui/dirlist/DragStartListenerTest.java +++ b/tests/unit/com/android/documentsui/dirlist/DragStartListenerTest.java @@ -32,8 +32,8 @@ import com.android.documentsui.base.Events.InputEvent; import com.android.documentsui.base.Providers; import com.android.documentsui.base.State; import com.android.documentsui.dirlist.DragStartListener.ActiveListener; +import com.android.documentsui.selection.DefaultSelectionManager; import com.android.documentsui.selection.Selection; -import com.android.documentsui.selection.SelectionManager; import com.android.documentsui.testing.SelectionManagers; import com.android.documentsui.testing.TestDragAndDropManager; import com.android.documentsui.testing.TestEvent; @@ -52,7 +52,7 @@ public class DragStartListenerTest { private ActiveListener mListener; private TestEvent.Builder mEvent; - private SelectionManager mMultiSelectManager; + private DefaultSelectionManager mMultiSelectManager; private SelectionDetails mSelectionDetails; private String mViewModelId; private TestDragAndDropManager mManager; diff --git a/tests/unit/com/android/documentsui/selection/BandControllerTest.java b/tests/unit/com/android/documentsui/selection/BandControllerTest.java index 6a3214743..fc056e694 100644 --- a/tests/unit/com/android/documentsui/selection/BandControllerTest.java +++ b/tests/unit/com/android/documentsui/selection/BandControllerTest.java @@ -16,36 +16,48 @@ package com.android.documentsui.selection; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + import android.graphics.Point; import android.graphics.Rect; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; import android.support.v7.widget.RecyclerView.OnScrollListener; -import android.test.AndroidTestCase; -import android.test.suitebuilder.annotation.SmallTest; import android.view.MotionEvent; import com.android.documentsui.DirectoryReloadLock; import com.android.documentsui.dirlist.TestData; import com.android.documentsui.dirlist.TestDocumentsAdapter; -import com.android.documentsui.dirlist.TestFocusHandler; import com.android.documentsui.testing.SelectionManagers; import com.android.documentsui.testing.TestEvent.Builder; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + import java.util.Collections; import java.util.List; +@RunWith(AndroidJUnit4.class) @SmallTest -public class BandControllerTest extends AndroidTestCase { +public class BandControllerTest { private static final List<String> ITEMS = TestData.create(10); private BandController mBandController; private boolean mIsActive; - @Override - public void setUp() throws Exception { + @Before + public void setup() throws Exception { mIsActive = false; - mBandController = new BandController(new TestSelectionEnvironment(), - new TestDocumentsAdapter(ITEMS), SelectionManagers.createTestInstance(ITEMS), - new DirectoryReloadLock(), null) { + mBandController = new BandController( + new TestSelectionEnvironment(), + new TestDocumentsAdapter(ITEMS), + SelectionManagers.createTestInstance(ITEMS), + SelectionManagers.CAN_SELECT_ANYTHING, + new DirectoryReloadLock(), + null // "gridItemTester" + ) { @Override public boolean isActive() { return mIsActive; @@ -53,100 +65,123 @@ public class BandControllerTest extends AndroidTestCase { }; } + @Test public void testGoodStart() { assertTrue(mBandController.shouldStart(goodStartEventBuilder().build())); } + @Test public void testBadStart_NoButtons() { assertFalse(mBandController.shouldStart( goodStartEventBuilder().releaseButton(MotionEvent.BUTTON_PRIMARY).build())); } + @Test public void testBadStart_SecondaryButton() { assertFalse( mBandController.shouldStart(goodStartEventBuilder().secondary().build())); } + @Test public void testBadStart_TertiaryButton() { assertFalse( mBandController.shouldStart(goodStartEventBuilder().tertiary().build())); } + @Test public void testBadStart_Touch() { assertFalse(mBandController.shouldStart( goodStartEventBuilder().touch().releaseButton(MotionEvent.BUTTON_PRIMARY).build())); } + @Test public void testBadStart_inDragSpot() { assertFalse( mBandController.shouldStart(goodStartEventBuilder().at(1).inDragHotspot().build())); } + @Test public void testBadStart_ActionDown() { assertFalse(mBandController .shouldStart(goodStartEventBuilder().action(MotionEvent.ACTION_DOWN).build())); } + @Test public void testBadStart_ActionUp() { assertFalse(mBandController .shouldStart(goodStartEventBuilder().action(MotionEvent.ACTION_UP).build())); } + @Test public void testBadStart_ActionPointerDown() { assertFalse(mBandController.shouldStart( goodStartEventBuilder().action(MotionEvent.ACTION_POINTER_DOWN).build())); } + @Test public void testBadStart_ActionPointerUp() { assertFalse(mBandController.shouldStart( goodStartEventBuilder().action(MotionEvent.ACTION_POINTER_UP).build())); } + @Test public void testBadStart_NoItems() { - mBandController = new BandController(new TestSelectionEnvironment(), + mBandController = new BandController( + new TestSelectionEnvironment(), new TestDocumentsAdapter(Collections.EMPTY_LIST), SelectionManagers.createTestInstance(ITEMS), - new DirectoryReloadLock(), null); + SelectionManagers.CAN_SELECT_ANYTHING, + new DirectoryReloadLock(), + null // gridItemTester + ); assertFalse(mBandController.shouldStart(goodStartEventBuilder().build())); } + @Test public void testBadStart_alreadyActive() { mIsActive = true; assertFalse(mBandController.shouldStart(goodStartEventBuilder().build())); } + @Test public void testGoodStop() { mIsActive = true; assertTrue(mBandController.shouldStop(goodStopEventBuilder().build())); } + @Test public void testGoodStop_PointerUp() { mIsActive = true; assertTrue(mBandController .shouldStop(goodStopEventBuilder().action(MotionEvent.ACTION_POINTER_UP).build())); } + @Test public void testGoodStop_Cancel() { mIsActive = true; assertTrue(mBandController .shouldStop(goodStopEventBuilder().action(MotionEvent.ACTION_CANCEL).build())); } + @Test public void testBadStop_NotActive() { assertFalse(mBandController.shouldStop(goodStopEventBuilder().build())); } + @Test public void testBadStop_NonMouse() { mIsActive = true; assertFalse(mBandController.shouldStop(goodStopEventBuilder().touch().build())); } + @Test public void testBadStop_Move() { mIsActive = true; assertFalse(mBandController.shouldStop( goodStopEventBuilder().action(MotionEvent.ACTION_MOVE).touch().build())); } + @Test public void testBadStop_Down() { mIsActive = true; assertFalse(mBandController.shouldStop( diff --git a/tests/unit/com/android/documentsui/selection/BandController_GridModelTest.java b/tests/unit/com/android/documentsui/selection/BandController_GridModelTest.java index 496d93532..b51228520 100644 --- a/tests/unit/com/android/documentsui/selection/BandController_GridModelTest.java +++ b/tests/unit/com/android/documentsui/selection/BandController_GridModelTest.java @@ -17,22 +17,30 @@ package com.android.documentsui.selection; import static com.android.documentsui.selection.BandController.GridModel.NOT_SET; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import android.graphics.Point; import android.graphics.Rect; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; import android.support.v7.widget.RecyclerView.OnScrollListener; -import android.test.AndroidTestCase; -import android.test.suitebuilder.annotation.SmallTest; import com.android.documentsui.dirlist.TestDocumentsAdapter; import com.android.documentsui.selection.BandController.GridModel; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; + import java.util.ArrayList; import java.util.List; import java.util.Set; +@RunWith(AndroidJUnit4.class) @SmallTest -public class BandController_GridModelTest extends AndroidTestCase { +public class BandController_GridModelTest { private static final int VIEW_PADDING_PX = 5; private static final int CHILD_VIEW_EDGE_PX = 100; @@ -55,43 +63,14 @@ public class BandController_GridModelTest extends AndroidTestCase { private Point mSelectionOrigin; private Point mSelectionPoint; - private void initData(final int numChildren, int numColumns) { - env = new TestEnvironment(numChildren, numColumns); - adapter = new TestDocumentsAdapter(new ArrayList<String>()) { - @Override - public String getStableId(int position) { - return Integer.toString(position); - } - - @Override - public int getItemCount() { - return numChildren; - } - }; - - viewWidth = VIEW_PADDING_PX + numColumns * (VIEW_PADDING_PX + CHILD_VIEW_EDGE_PX); - model = new GridModel(env, (int pos) -> true, adapter); - model.addOnSelectionChangedListener( - new GridModel.OnSelectionChangedListener() { - @Override - public void onSelectionChanged(Set<String> updatedSelection) { - lastSelection = updatedSelection; - } - - @Override - public boolean onBeforeItemStateChange(String id, boolean nextState) { - return true; - } - }); - } - - @Override + @After public void tearDown() { model = null; env = null; lastSelection = null; } + @Test public void testSelectionLeftOfItems() { initData(20, 5); startSelection(new Point(0, 10)); @@ -100,6 +79,7 @@ public class BandController_GridModelTest extends AndroidTestCase { assertEquals(NOT_SET, model.getPositionNearestOrigin()); } + @Test public void testSelectionRightOfItems() { initData(20, 4); startSelection(new Point(viewWidth - 1, 10)); @@ -108,6 +88,7 @@ public class BandController_GridModelTest extends AndroidTestCase { assertEquals(NOT_SET, model.getPositionNearestOrigin()); } + @Test public void testSelectionAboveItems() { initData(20, 4); startSelection(new Point(10, 0)); @@ -116,6 +97,7 @@ public class BandController_GridModelTest extends AndroidTestCase { assertEquals(NOT_SET, model.getPositionNearestOrigin()); } + @Test public void testSelectionBelowItems() { initData(5, 4); startSelection(new Point(10, VIEWPORT_HEIGHT - 1)); @@ -124,6 +106,7 @@ public class BandController_GridModelTest extends AndroidTestCase { assertEquals(NOT_SET, model.getPositionNearestOrigin()); } + @Test public void testVerticalSelectionBetweenItems() { initData(20, 4); startSelection(new Point(106, 0)); @@ -132,6 +115,7 @@ public class BandController_GridModelTest extends AndroidTestCase { assertEquals(NOT_SET, model.getPositionNearestOrigin()); } + @Test public void testHorizontalSelectionBetweenItems() { initData(20, 4); startSelection(new Point(0, 105)); @@ -140,6 +124,7 @@ public class BandController_GridModelTest extends AndroidTestCase { assertEquals(NOT_SET, model.getPositionNearestOrigin()); } + @Test public void testGrowingAndShrinkingSelection() { initData(20, 4); startSelection(new Point(0, 0)); @@ -183,6 +168,7 @@ public class BandController_GridModelTest extends AndroidTestCase { assertEquals(NOT_SET, model.getPositionNearestOrigin()); } + @Test public void testSelectionMovingAroundOrigin() { initData(16, 4); @@ -204,6 +190,7 @@ public class BandController_GridModelTest extends AndroidTestCase { assertEquals(7, model.getPositionNearestOrigin()); } + @Test public void testScrollingBandSelect() { initData(40, 4); @@ -229,6 +216,36 @@ public class BandController_GridModelTest extends AndroidTestCase { assertEquals(0, model.getPositionNearestOrigin()); } + private void initData(final int numChildren, int numColumns) { + env = new TestEnvironment(numChildren, numColumns); + adapter = new TestDocumentsAdapter(new ArrayList<String>()) { + @Override + public String getStableId(int position) { + return Integer.toString(position); + } + + @Override + public int getItemCount() { + return numChildren; + } + }; + + viewWidth = VIEW_PADDING_PX + numColumns * (VIEW_PADDING_PX + CHILD_VIEW_EDGE_PX); + model = new GridModel(env, (int pos) -> true, adapter); + model.addOnSelectionChangedListener( + new GridModel.OnSelectionChangedListener() { + @Override + public void onSelectionChanged(Set<String> updatedSelection) { + lastSelection = updatedSelection; + } + + @Override + public boolean onBeforeItemStateChange(String id, boolean nextState) { + return true; + } + }); + } + /** Returns the current selection area as a Rect. */ private Rect getSelectionArea() { // Construct a rect from the two selection points. @@ -458,6 +475,7 @@ public class BandController_GridModelTest extends AndroidTestCase { rect = r; } + @Override public String toString() { return name + ": " + rect; } diff --git a/tests/unit/com/android/documentsui/selection/SelectionManagerTest.java b/tests/unit/com/android/documentsui/selection/DefaultSelectionManagerTest.java index 8d7e2b5be..6ec2a2c0d 100644 --- a/tests/unit/com/android/documentsui/selection/SelectionManagerTest.java +++ b/tests/unit/com/android/documentsui/selection/DefaultSelectionManagerTest.java @@ -20,11 +20,9 @@ import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; import android.util.SparseBooleanArray; -import com.android.documentsui.dirlist.TestDocumentsAdapter; -import com.android.documentsui.selection.SelectionManager; import com.android.documentsui.dirlist.TestData; -import com.android.documentsui.selection.Selection; -import com.android.documentsui.testing.SelectionManagers; +import com.android.documentsui.dirlist.TestDocumentsAdapter; +import com.android.documentsui.selection.SelectionManager.SelectionPredicate; import org.junit.Before; import org.junit.Test; @@ -37,13 +35,13 @@ import java.util.Set; @RunWith(AndroidJUnit4.class) @SmallTest -public class SelectionManagerTest { +public class DefaultSelectionManagerTest { private static final List<String> ITEMS = TestData.create(100); private final Set<String> mIgnored = new HashSet<>(); private TestDocumentsAdapter mAdapter; - private SelectionManager mManager; + private DefaultSelectionManager mManager; private TestSelectionEventListener mListener; private SelectionProbe mSelection; @@ -51,10 +49,16 @@ public class SelectionManagerTest { public void setUp() throws Exception { mListener = new TestSelectionEventListener(); mAdapter = new TestDocumentsAdapter(ITEMS); - mManager = SelectionManagers.createTestInstance( - mAdapter, - SelectionManager.MODE_MULTIPLE, - (String id, boolean nextState) -> (!nextState || !mIgnored.contains(id))); + mManager = new DefaultSelectionManager(SelectionManager.MODE_MULTIPLE); + SelectionManager.SelectionPredicate canSelect = new SelectionManager.SelectionPredicate() { + + @Override + public boolean test(String id, boolean nextState) { + return !nextState || !mIgnored.contains(id); + } + + }; + mManager.reset(mAdapter, mAdapter, canSelect); mManager.addEventListener(mListener); mSelection = new SelectionProbe(mManager, mListener); diff --git a/tests/unit/com/android/documentsui/selection/SelectionManager_SingleSelectTest.java b/tests/unit/com/android/documentsui/selection/DefaultSelectionManager_SingleSelectTest.java index a7d3caab4..ba7692a21 100644 --- a/tests/unit/com/android/documentsui/selection/SelectionManager_SingleSelectTest.java +++ b/tests/unit/com/android/documentsui/selection/DefaultSelectionManager_SingleSelectTest.java @@ -33,7 +33,7 @@ import java.util.List; @RunWith(AndroidJUnit4.class) @SmallTest -public class SelectionManager_SingleSelectTest { +public class DefaultSelectionManager_SingleSelectTest { private static final List<String> ITEMS = TestData.create(100); diff --git a/tests/unit/com/android/documentsui/selection/GestureSelectorTest.java b/tests/unit/com/android/documentsui/selection/GestureSelectorTest.java index 85a857a99..326b16207 100644 --- a/tests/unit/com/android/documentsui/selection/GestureSelectorTest.java +++ b/tests/unit/com/android/documentsui/selection/GestureSelectorTest.java @@ -16,15 +16,22 @@ package com.android.documentsui.selection; -import android.test.AndroidTestCase; -import android.test.suitebuilder.annotation.SmallTest; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; import android.view.View; -import com.android.documentsui.selection.GestureSelector; import com.android.documentsui.testing.TestEvent; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) @SmallTest -public class GestureSelectorTest extends AndroidTestCase { +public class GestureSelectorTest { TestEvent.Builder e; @@ -34,29 +41,33 @@ public class GestureSelectorTest extends AndroidTestCase { static final int TOP_BORDER = 20; static final int BOTTOM_BORDER = 40; - @Override - public void setUp() throws Exception { + @Before + public void setup() throws Exception { e = TestEvent.builder() .location(100, 100); } + @Test public void testLTRPastLastItem() { assertTrue(GestureSelector.isPastLastItem( TOP_BORDER, LEFT_BORDER, RIGHT_BORDER, e.build(), View.LAYOUT_DIRECTION_LTR)); } + @Test public void testLTRPastLastItem_Inverse() { e.location(10, 10); assertFalse(GestureSelector.isPastLastItem( TOP_BORDER, LEFT_BORDER, RIGHT_BORDER, e.build(), View.LAYOUT_DIRECTION_LTR)); } + @Test public void testRTLPastLastItem() { e.location(10, 30); assertTrue(GestureSelector.isPastLastItem( TOP_BORDER, LEFT_BORDER, RIGHT_BORDER, e.build(), View.LAYOUT_DIRECTION_RTL)); } + @Test public void testRTLPastLastItem_Inverse() { assertFalse(GestureSelector.isPastLastItem( TOP_BORDER, LEFT_BORDER, RIGHT_BORDER, e.build(), View.LAYOUT_DIRECTION_RTL)); |