diff options
author | 2016-09-12 14:18:32 -0700 | |
---|---|---|
committer | 2016-09-13 15:57:02 -0700 | |
commit | 84bd0f13bd02ea26acc937b00c050884dc7546ef (patch) | |
tree | c3929141d78f737c50b0ceb3b0f5100df4814a8c | |
parent | ba7e6c4d145d678968dd3cc5bf3469b357d91524 (diff) |
Refactor SelectionModeListener.
* Move ActionMode logic into a new class ActionModeController
* Move SelectionDetails logic into MultiSelectionController
* Merge canSelect() and onBeforeItemStateChange()
* Add some basic unit tests for selecting unselectable items
* Fix a bug that selects unselectable items using gestural selection
* Convert MultiSelectManagerTests to JUnit4
Change-Id: I14642178ff39e7b990cc9f3fb0d9f40e6309e087
23 files changed, 654 insertions, 354 deletions
diff --git a/app-perf-tests/src/com/android/documentsui/LauncherActivity.java b/app-perf-tests/src/com/android/documentsui/LauncherActivity.java index 21fc52e00..21ea8aa94 100644 --- a/app-perf-tests/src/com/android/documentsui/LauncherActivity.java +++ b/app-perf-tests/src/com/android/documentsui/LauncherActivity.java @@ -20,10 +20,8 @@ import static com.android.documentsui.Shared.EXTRA_BENCHMARK; import android.app.Activity; import android.content.Intent; -import android.content.pm.PackageManager; import android.os.Bundle; import android.os.Handler; -import android.util.Log; import java.util.concurrent.CountDownLatch; diff --git a/src/com/android/documentsui/Events.java b/src/com/android/documentsui/Events.java index 3515a94fc..98cf01aa1 100644 --- a/src/com/android/documentsui/Events.java +++ b/src/com/android/documentsui/Events.java @@ -328,9 +328,4 @@ public final class Events { .toString(); } } - - @FunctionalInterface - public interface EventHandler { - boolean apply(InputEvent event); - } } diff --git a/src/com/android/documentsui/MenuManager.java b/src/com/android/documentsui/MenuManager.java index 8b2880d9f..19d4482f8 100644 --- a/src/com/android/documentsui/MenuManager.java +++ b/src/com/android/documentsui/MenuManager.java @@ -16,8 +16,6 @@ package com.android.documentsui; -import android.annotation.Nullable; -import android.support.v7.widget.RecyclerView; import android.view.Menu; import android.view.MenuItem; diff --git a/src/com/android/documentsui/OpenExternalDirectoryActivity.java b/src/com/android/documentsui/OpenExternalDirectoryActivity.java index 6588ee1c9..245f902e1 100644 --- a/src/com/android/documentsui/OpenExternalDirectoryActivity.java +++ b/src/com/android/documentsui/OpenExternalDirectoryActivity.java @@ -17,12 +17,10 @@ package com.android.documentsui; import static android.os.Environment.isStandardDirectory; -import static android.os.Environment.STANDARD_DIRECTORIES; import static android.os.storage.StorageVolume.EXTRA_DIRECTORY_NAME; import static android.os.storage.StorageVolume.EXTRA_STORAGE_VOLUME; import static com.android.documentsui.LocalPreferences.getScopedAccessPermissionStatus; -import static com.android.documentsui.LocalPreferences.PERMISSION_ASK; import static com.android.documentsui.LocalPreferences.PERMISSION_ASK_AGAIN; import static com.android.documentsui.LocalPreferences.PERMISSION_NEVER_ASK; import static com.android.documentsui.LocalPreferences.setScopedAccessPermissionStatus; diff --git a/src/com/android/documentsui/QuickViewIntentBuilder.java b/src/com/android/documentsui/QuickViewIntentBuilder.java index 5e3bbbb86..b8d224713 100644 --- a/src/com/android/documentsui/QuickViewIntentBuilder.java +++ b/src/com/android/documentsui/QuickViewIntentBuilder.java @@ -22,7 +22,6 @@ import static com.android.documentsui.model.DocumentInfo.getCursorString; import android.content.ClipData; import android.content.ClipDescription; -import android.content.ComponentName; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Resources; diff --git a/src/com/android/documentsui/base/FunctionalInterfaces.java b/src/com/android/documentsui/base/FunctionalInterfaces.java new file mode 100644 index 000000000..936883a84 --- /dev/null +++ b/src/com/android/documentsui/base/FunctionalInterfaces.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.documentsui.base; + +/** + * A container class that contains common functional interfaces used in DocumentsUI. + * + * This class should never be instantiated. + */ +public class FunctionalInterfaces { + + private FunctionalInterfaces() {} + + /** + * A functional interface that handles an event and returns a boolean to indicate if the event + * is consumed. + */ + @FunctionalInterface + public interface EventHandler<T> { + boolean apply(T event); + } +}
\ No newline at end of file diff --git a/src/com/android/documentsui/dirlist/ActionModeController.java b/src/com/android/documentsui/dirlist/ActionModeController.java new file mode 100644 index 000000000..80387680b --- /dev/null +++ b/src/com/android/documentsui/dirlist/ActionModeController.java @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.documentsui.dirlist; + +import static com.android.documentsui.Shared.DEBUG; + +import android.annotation.IdRes; +import android.annotation.Nullable; +import android.app.Activity; +import android.content.Context; +import android.text.TextUtils; +import android.util.Log; +import android.view.ActionMode; +import android.view.HapticFeedbackConstants; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; + +import com.android.documentsui.MenuManager; +import com.android.documentsui.Menus; +import com.android.documentsui.R; +import com.android.documentsui.base.FunctionalInterfaces.EventHandler; +import com.android.documentsui.Shared; +import com.android.documentsui.dirlist.MultiSelectManager.Selection; + +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.IntConsumer; + +/** + * A controller that listens to selection changes and manages life cycles of action modes. + */ +class ActionModeController implements MultiSelectManager.Callback, ActionMode.Callback { + + private static final String TAG = "ActionModeController"; + + private final Context mContext; + private final MultiSelectManager mSelectionMgr; + private final MenuManager mMenuManager; + private final MenuManager.SelectionDetails mSelectionDetails; + + private final Function<ActionMode.Callback, ActionMode> mActionModeFactory; + private final EventHandler<MenuItem> mMenuItemClicker; + private final IntConsumer mHapticPerformer; + private final Consumer<CharSequence> mAccessibilityAnnouncer; + private final AccessibilityImportanceSetter mAccessibilityImportanceSetter; + + private final Selection mSelected = new Selection(); + + private @Nullable ActionMode mActionMode; + private @Nullable Menu mMenu; + + private ActionModeController( + Context context, + MultiSelectManager selectionMgr, + MenuManager menuManager, + MenuManager.SelectionDetails selectionDetails, + Function<ActionMode.Callback, ActionMode> actionModeFactory, + EventHandler<MenuItem> menuItemClicker, + IntConsumer hapticPerformer, + Consumer<CharSequence> accessibilityAnnouncer, + AccessibilityImportanceSetter accessibilityImportanceSetter) { + mContext = context; + mSelectionMgr = selectionMgr; + mMenuManager = menuManager; + mSelectionDetails = selectionDetails; + + mActionModeFactory = actionModeFactory; + mMenuItemClicker = menuItemClicker; + mHapticPerformer = hapticPerformer; + mAccessibilityAnnouncer = accessibilityAnnouncer; + mAccessibilityImportanceSetter = accessibilityImportanceSetter; + } + + @Override + public void onSelectionChanged() { + mSelectionMgr.getSelection(mSelected); + if (mSelected.size() > 0) { + if (mActionMode == null) { + if (DEBUG) Log.d(TAG, "Starting action mode."); + mActionMode = mActionModeFactory.apply(this); + mHapticPerformer.accept(HapticFeedbackConstants.LONG_PRESS); + } + updateActionMenu(); + } else { + if (mActionMode != null) { + if (DEBUG) Log.d(TAG, "Finishing action mode."); + mActionMode.finish(); + } + } + + if (mActionMode != null) { + assert(!mSelected.isEmpty()); + final String title = Shared.getQuantityString(mContext, + R.plurals.elements_selected, mSelected.size()); + mActionMode.setTitle(title); + mAccessibilityAnnouncer.accept(title); + } + } + + @Override + public void onSelectionRestored() { + mSelectionMgr.getSelection(mSelected); + if (mSelected.size() > 0) { + if (mActionMode == null) { + if (DEBUG) Log.d(TAG, "Starting action mode."); + mActionMode = mActionModeFactory.apply(this); + } + updateActionMenu(); + } else { + if (mActionMode != null) { + if (DEBUG) Log.d(TAG, "Finishing action mode."); + mActionMode.finish(); + } + } + + if (mActionMode != null) { + assert(!mSelected.isEmpty()); + final String title = Shared.getQuantityString(mContext, + R.plurals.elements_selected, mSelected.size()); + mActionMode.setTitle(title); + mAccessibilityAnnouncer.accept(title); + } + } + + void finishActionMode() { + if (mActionMode != null) { + mActionMode.finish(); + mActionMode = null; + } else { + Log.w(TAG, "Tried to finish a null action mode."); + } + } + + // Called when the user exits the action mode + @Override + public void onDestroyActionMode(ActionMode mode) { + if (DEBUG) Log.d(TAG, "Handling action mode destroyed."); + mActionMode = null; + // clear selection + mSelectionMgr.clearSelection(); + mSelected.clear(); + + // Re-enable TalkBack for the toolbars, as they are no longer covered by action mode. + mAccessibilityImportanceSetter.setAccessibilityImportance( + View.IMPORTANT_FOR_ACCESSIBILITY_AUTO, R.id.toolbar, R.id.roots_toolbar); + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + int size = mSelectionMgr.getSelection().size(); + mode.getMenuInflater().inflate(R.menu.mode_directory, menu); + mode.setTitle(TextUtils.formatSelectedCount(size)); + + if (size > 0) { + + // Hide the toolbars if action mode is enabled, so TalkBack doesn't navigate to + // these controls when using linear navigation. + mAccessibilityImportanceSetter.setAccessibilityImportance( + View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS, + R.id.toolbar, + R.id.roots_toolbar); + return true; + } + + return false; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + mMenu = menu; + updateActionMenu(); + return true; + } + + private void updateActionMenu() { + assert(mMenu != null); + mMenuManager.updateActionMenu(mMenu, mSelectionDetails); + Menus.disableHiddenItems(mMenu); + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + return mMenuItemClicker.apply(item); + } + + static ActionModeController create( + Context context, + MultiSelectManager selectionMgr, + MenuManager menuManager, + MenuManager.SelectionDetails selectionDetails, + Activity activity, + View view, + EventHandler<MenuItem> menuItemClicker) { + return new ActionModeController( + context, + selectionMgr, + menuManager, + selectionDetails, + activity::startActionMode, + menuItemClicker, + view::performHapticFeedback, + view::announceForAccessibility, + (int accessibilityImportance, @IdRes int[] viewIds) -> { + setImportantForAccessibility(activity, accessibilityImportance, viewIds); + }); + } + + private static void setImportantForAccessibility( + Activity activity, int accessibilityImportance, @IdRes int[] viewIds) { + for (final int id : viewIds) { + final View v = activity.findViewById(id); + if (v != null) { + v.setImportantForAccessibility(accessibilityImportance); + } + } + } + + @FunctionalInterface + private interface AccessibilityImportanceSetter { + void setAccessibilityImportance(int accessibilityImportance, @IdRes int... viewIds); + } +} diff --git a/src/com/android/documentsui/dirlist/BandController.java b/src/com/android/documentsui/dirlist/BandController.java index 1df0cfe4c..4e9a872b1 100644 --- a/src/com/android/documentsui/dirlist/BandController.java +++ b/src/com/android/documentsui/dirlist/BandController.java @@ -33,12 +33,9 @@ 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 com.android.documentsui.dirlist.ViewAutoScroller.ScrollActionDelegate; @@ -328,7 +325,7 @@ public class BandController extends OnScrollListener { } private boolean onBeforeItemStateChange(String id, boolean nextState) { - return mSelectionManager.notifyBeforeItemStateChange(id, nextState); + return mSelectionManager.canSetState(id, nextState); } @Override diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java index 30ccb2868..fe0795869 100644 --- a/src/com/android/documentsui/dirlist/DirectoryFragment.java +++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java @@ -50,13 +50,10 @@ import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView.RecyclerListener; import android.support.v7.widget.RecyclerView.ViewHolder; import android.text.BidiFormatter; -import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; -import android.view.ActionMode; import android.view.ContextMenu; import android.view.DragEvent; -import android.view.HapticFeedbackConstants; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -66,7 +63,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; -import android.widget.Toolbar; import com.android.documentsui.BaseActivity; import com.android.documentsui.DirectoryLoader; @@ -77,10 +73,8 @@ import com.android.documentsui.Events.InputEvent; import com.android.documentsui.Events.MotionInputEvent; import com.android.documentsui.ItemDragListener; import com.android.documentsui.MenuManager; -import com.android.documentsui.Menus; import com.android.documentsui.MessageBar; import com.android.documentsui.Metrics; -import com.android.documentsui.MimePredicate; import com.android.documentsui.R; import com.android.documentsui.RecentsLoader; import com.android.documentsui.RetainedState; @@ -108,7 +102,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; -import java.util.Objects; import javax.annotation.Nullable; @@ -143,8 +136,9 @@ public class DirectoryFragment extends Fragment private final Model mModel = new Model(); private final Model.UpdateListener mModelUpdateListener = new ModelUpdateListener(); - private final SelectionModeListener mSelectionModeListener = new SelectionModeListener(); private MultiSelectManager mSelectionMgr; + private ActionModeController mActionModeController; + private SelectionMetadata mSelectionMetadata; private UserInputHandler<InputEvent> mInputHandler; private FocusManager mFocusManager; @@ -183,7 +177,6 @@ public class DirectoryFragment extends Fragment private boolean mSearchMode = false; private @Nullable BandController mBandController; - private @Nullable ActionMode mActionMode; private DragHoverListener mDragHoverListener; private MenuManager mMenuManager; @@ -301,7 +294,10 @@ public class DirectoryFragment extends Fragment mAdapter, state.allowMultiple ? MultiSelectManager.MODE_MULTIPLE - : MultiSelectManager.MODE_SINGLE); + : MultiSelectManager.MODE_SINGLE, + this::canSetSelectionState); + mSelectionMetadata = new SelectionMetadata(mSelectionMgr, mModel::getItem); + mSelectionMgr.addItemCallback(mSelectionMetadata); mModel.addUpdateListener(mAdapter); mModel.addUpdateListener(mModelUpdateListener); @@ -355,7 +351,19 @@ public class DirectoryFragment extends Fragment mInputHandler, mBandController); - mSelectionMgr.addCallback(mSelectionModeListener); + final BaseActivity activity = getBaseActivity(); + mTuner = activity.createFragmentTuner(); + mMenuManager = activity.getMenuManager(); + + mActionModeController = ActionModeController.create( + getContext(), + mSelectionMgr, + mMenuManager, + mSelectionMetadata, + getActivity(), + mRecView, + this::handleMenuItemClick); + mSelectionMgr.addCallback(mActionModeController); final ActivityManager am = (ActivityManager) context.getSystemService( Context.ACTIVITY_SERVICE); @@ -440,7 +448,9 @@ public class DirectoryFragment extends Fragment boolean mouseOverFile = !(v == mRecView || v == mEmptyView); if (mouseOverFile) { mMenuManager.updateContextMenuForFile( - menu, mSelectionModeListener, getBaseActivity().getDirectoryDetails()); + menu, + mSelectionMetadata, + getBaseActivity().getDirectoryDetails()); } else { mMenuManager.updateContextMenuForContainer( menu, getBaseActivity().getDirectoryDetails()); @@ -593,220 +603,19 @@ public class DirectoryFragment extends Fragment return (BaseActivity) getActivity(); } - /** - * Manages the integration between our ActionMode and MultiSelectManager, initiating - * ActionMode when there is a selection, canceling it when there is no selection, - * and clearing selection when action mode is explicitly exited by the user. - */ - private final class SelectionModeListener implements MultiSelectManager.Callback, - ActionMode.Callback, MenuManager.SelectionDetails { - - private Selection mSelected = new Selection(); - - // Partial files are files that haven't been fully downloaded. - private int mPartialCount = 0; - private int mDirectoryCount = 0; - private int mWritableDirectoryCount = 0; - private int mNoDeleteCount = 0; - private int mNoRenameCount = 0; - - private Menu mMenu; - - @Override - public boolean onBeforeItemStateChange(String modelId, boolean selected) { - if (selected) { - final Cursor cursor = mModel.getItem(modelId); - if (cursor == null) { - Log.w(TAG, "Can't obtain cursor for modelId: " + modelId); - return false; - } - - final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); - final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS); - if (!mTuner.canSelectType(docMimeType, docFlags)) { - return false; - } - return mTuner.canSelectType(docMimeType, docFlags); - } - return true; - } - - @Override - public void onItemStateChanged(String modelId, boolean selected) { - final Cursor cursor = mModel.getItem(modelId); - if (cursor == null) { - Log.w(TAG, "Model returned null cursor for document: " + modelId - + ". Ignoring state changed event."); - return; - } - - // TODO: Should this be happening in onSelectionChanged? Technically this callback is - // triggered on "silent" selection updates (i.e. we might be reacting to unfinalized - // selection changes here) - final String mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); - if (MimePredicate.isDirectoryType(mimeType)) { - mDirectoryCount += selected ? 1 : -1; - } - - final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS); - if ((docFlags & Document.FLAG_PARTIAL) != 0) { - mPartialCount += selected ? 1 : -1; - } - if ((docFlags & Document.FLAG_DIR_SUPPORTS_CREATE) != 0) { - mWritableDirectoryCount += selected ? 1 : -1; - } - if ((docFlags & Document.FLAG_SUPPORTS_DELETE) == 0) { - mNoDeleteCount += selected ? 1 : -1; - } - if ((docFlags & Document.FLAG_SUPPORTS_RENAME) == 0) { - mNoRenameCount += selected ? 1 : -1; - } - } - - @Override - public void onSelectionChanged() { - mSelectionMgr.getSelection(mSelected); - if (mSelected.size() > 0) { - if (mActionMode == null) { - if (DEBUG) Log.d(TAG, "Starting action mode."); - mActionMode = getActivity().startActionMode(this); - } - updateActionMenu(); - } else { - if (mActionMode != null) { - if (DEBUG) Log.d(TAG, "Finishing action mode."); - mActionMode.finish(); - } - } - - if (mActionMode != null) { - assert(!mSelected.isEmpty()); - final String title = Shared.getQuantityString(getActivity(), - R.plurals.elements_selected, mSelected.size()); - mActionMode.setTitle(title); - mRecView.announceForAccessibility(title); - } - } - - // Called when the user exits the action mode - @Override - public void onDestroyActionMode(ActionMode mode) { - if (DEBUG) Log.d(TAG, "Handling action mode destroyed."); - mActionMode = null; - // clear selection - mSelectionMgr.clearSelection(); - mSelected.clear(); - - mDirectoryCount = 0; - mPartialCount = 0; - mNoDeleteCount = 0; - mNoRenameCount = 0; - - // Re-enable TalkBack for the toolbars, as they are no longer covered by action mode. - final Toolbar toolbar = (Toolbar) getActivity().findViewById(R.id.toolbar); - toolbar.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO); - - // This toolbar is not present in the fixed_layout - final Toolbar rootsToolbar = (Toolbar) getActivity().findViewById(R.id.roots_toolbar); - if (rootsToolbar != null) { - rootsToolbar.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO); - } - } - - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - if (mRestoredSelection != null) { - // This is a careful little song and dance to avoid haptic feedback - // when selection has been restored after rotation. We're - // also responsible for cleaning up restored selection so the - // object dones't unnecessarily hang around. - mRestoredSelection = null; - } else { - mRecView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); - } - - int size = mSelectionMgr.getSelection().size(); - mode.getMenuInflater().inflate(R.menu.mode_directory, menu); - mode.setTitle(TextUtils.formatSelectedCount(size)); - - if (size > 0) { - // Hide the toolbars if action mode is enabled, so TalkBack doesn't navigate to - // these controls when using linear navigation. - final Toolbar toolbar = (Toolbar) getActivity().findViewById(R.id.toolbar); - toolbar.setImportantForAccessibility( - View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); - - // This toolbar is not present in the fixed_layout - final Toolbar rootsToolbar = (Toolbar) getActivity().findViewById( - R.id.roots_toolbar); - if (rootsToolbar != null) { - rootsToolbar.setImportantForAccessibility( - View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); - } - return true; - } - - return false; - } - - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - mMenu = menu; - updateActionMenu(); - return true; - } - - @Override - public boolean containsDirectories() { - return mDirectoryCount > 0; - } - - @Override - public boolean containsPartialFiles() { - return mPartialCount > 0; - } - - @Override - public boolean canDelete() { - return mNoDeleteCount == 0; - } - - @Override - public boolean canRename() { - return mNoRenameCount == 0 && mSelectionMgr.getSelection().size() == 1; - } - - @Override - public boolean canPasteInto() { - return mDirectoryCount == 1 && mWritableDirectoryCount == 1 - && mSelectionMgr.getSelection().size() == 1; - } - - private void updateActionMenu() { - assert(mMenu != null); - mMenuManager.updateActionMenu(mMenu, this); - Menus.disableHiddenItems(mMenu); - } - - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - return handleMenuItemClick(item); - } - } - private boolean handleMenuItemClick(MenuItem item) { Selection selection = mSelectionMgr.getSelection(new Selection()); switch (item.getItemId()) { case R.id.menu_open: openDocuments(selection); - mActionMode.finish(); + mActionModeController.finishActionMode(); return true; case R.id.menu_share: shareDocuments(selection); // TODO: Only finish selection if share action is completed. - mActionMode.finish(); + mActionModeController.finishActionMode(); return true; case R.id.menu_delete: @@ -819,12 +628,12 @@ public class DirectoryFragment extends Fragment transferDocuments(selection, FileOperationService.OPERATION_COPY); // TODO: Only finish selection mode if copy-to is not canceled. // Need to plum down into handling the way we do with deleteDocuments. - mActionMode.finish(); + mActionModeController.finishActionMode(); return true; case R.id.menu_move_to: // Exit selection mode first, so we avoid deselecting deleted documents. - mActionMode.finish(); + mActionModeController.finishActionMode(); transferDocuments(selection, FileOperationService.OPERATION_MOVE); return true; @@ -851,7 +660,7 @@ public class DirectoryFragment extends Fragment case R.id.menu_rename: // Exit selection mode first, so we avoid deselecting deleted // (renamed) documents. - mActionMode.finish(); + mActionModeController.finishActionMode(); renameDocuments(selection); return true; @@ -1013,11 +822,7 @@ public class DirectoryFragment extends Fragment // This is done here, rather in the onActionItemClicked // so we can avoid de-selecting items in the case where // the user cancels the delete. - if (mActionMode != null) { - mActionMode.finish(); - } else { - Log.w(TAG, "Action mode is null before deleting documents."); - } + mActionModeController.finishActionMode(); UrisSupplier srcs; try { @@ -1312,9 +1117,7 @@ public class DirectoryFragment extends Fragment // When files are selected for dragging, ActionMode is started. This obscures the breadcrumb // with an ActionBar. In order to make drag and drop to the breadcrumb possible, we first // end ActionMode so the breadcrumb is visible to the user. - if (mActionMode != null) { - mActionMode.finish(); - } + mActionModeController.finishActionMode(); } void dragStopped(boolean result) { @@ -1475,22 +1278,25 @@ public class DirectoryFragment extends Fragment } private boolean canSelect(DocumentDetails doc) { - return canSelect(doc.getModelId()); + return canSetSelectionState(doc.getModelId(), true); } - private boolean canSelect(String modelId) { + private boolean canSetSelectionState(String modelId, boolean nextState) { + if (nextState) { + // Check if an item can be selected + final Cursor cursor = mModel.getItem(modelId); + if (cursor == null) { + Log.w(TAG, "Couldn't obtain cursor for modelId: " + modelId); + return false; + } - // TODO: Combine this method with onBeforeItemStateChange, as both of them are almost - // the same, and responsible for the same thing (whether to select or not). - final Cursor cursor = mModel.getItem(modelId); - if (cursor == null) { - Log.w(TAG, "Couldn't obtain cursor for modelId: " + modelId); - return false; + final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); + final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS); + return mTuner.canSelectType(docMimeType, docFlags); + } else { + // Right now all selected items can be deselected. + return true; } - - final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); - final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS); - return mTuner.canSelectType(docMimeType, docFlags); } public static void showDirectory( diff --git a/src/com/android/documentsui/dirlist/DocumentsAdapter.java b/src/com/android/documentsui/dirlist/DocumentsAdapter.java index 4b3544799..cb5afd7d4 100644 --- a/src/com/android/documentsui/dirlist/DocumentsAdapter.java +++ b/src/com/android/documentsui/dirlist/DocumentsAdapter.java @@ -35,7 +35,7 @@ import java.util.List; * dummy layout objects was error prone when interspersed with the core mode / adapter code. * * @see ModelBackedDocumentsAdapter - * @see SectionBreakDocumentsAdapter + * @see SectionBreakDocumentsAdapterWrapper */ abstract class DocumentsAdapter extends RecyclerView.Adapter<DocumentHolder> diff --git a/src/com/android/documentsui/dirlist/ListeningGestureDetector.java b/src/com/android/documentsui/dirlist/ListeningGestureDetector.java index 4cf8455c9..514c03057 100644 --- a/src/com/android/documentsui/dirlist/ListeningGestureDetector.java +++ b/src/com/android/documentsui/dirlist/ListeningGestureDetector.java @@ -26,9 +26,9 @@ import android.view.View; import android.view.View.OnTouchListener; import com.android.documentsui.Events; -import com.android.documentsui.Events.EventHandler; import com.android.documentsui.Events.InputEvent; import com.android.documentsui.Events.MotionInputEvent; +import com.android.documentsui.base.FunctionalInterfaces.EventHandler; //Receives event meant for both directory and empty view, and either pass them to //{@link UserInputHandler} for simple gestures (Single Tap, Long-Press), or intercept them for @@ -37,7 +37,7 @@ final class ListeningGestureDetector extends GestureDetector implements OnItemTouchListener, OnTouchListener { private final GestureSelector mGestureSelector; - private final EventHandler mMouseDragListener; + private final EventHandler<InputEvent> mMouseDragListener; private final BandController mBandController; private final MouseDelegate mMouseDelegate = new MouseDelegate(); private final TouchDelegate mTouchDelegate = new TouchDelegate(); @@ -46,7 +46,7 @@ final class ListeningGestureDetector extends GestureDetector Context context, RecyclerView recView, View emptyView, - EventHandler mouseDragListener, + EventHandler<InputEvent> mouseDragListener, GestureSelector gestureSelector, UserInputHandler<? extends InputEvent> handler, @Nullable BandController bandController) { diff --git a/src/com/android/documentsui/dirlist/MultiSelectManager.java b/src/com/android/documentsui/dirlist/MultiSelectManager.java index 0a2cd4206..780c56400 100644 --- a/src/com/android/documentsui/dirlist/MultiSelectManager.java +++ b/src/com/android/documentsui/dirlist/MultiSelectManager.java @@ -68,17 +68,25 @@ public final class MultiSelectManager { private final Selection mSelection = new Selection(); private final DocumentsAdapter mAdapter; - private final List<MultiSelectManager.Callback> mCallbacks = new ArrayList<>(1); + private final List<Callback> mCallbacks = new ArrayList<>(1); + private final List<ItemCallback> mItemCallbacks = new ArrayList<>(1); private @Nullable Range mRanger; private boolean mSingleSelect; - public MultiSelectManager(DocumentsAdapter adapter, @SelectionMode int mode) { + private final SelectionPredicate mCanSetState; + + public MultiSelectManager( + DocumentsAdapter adapter, + @SelectionMode int mode, + SelectionPredicate canSetState) { assert(adapter != null); mAdapter = adapter; + mCanSetState = canSetState; + mSingleSelect = mode == MODE_SINGLE; mAdapter.registerAdapterDataObserver( new RecyclerView.AdapterDataObserver() { @@ -133,10 +141,16 @@ public final class MultiSelectManager { * * @param callback */ - public void addCallback(MultiSelectManager.Callback callback) { + public void addCallback(Callback callback) { + assert(callback != null); mCallbacks.add(callback); } + public void addItemCallback(ItemCallback itemCallback) { + assert(itemCallback != null); + mItemCallbacks.add(itemCallback); + } + public boolean hasSelection() { return !mSelection.isEmpty(); } @@ -172,12 +186,13 @@ public final class MultiSelectManager { } /** - * Returns an unordered array of selected positions, including any - * provisional selection currently in effect. + * Restores the selected state of specified items. Used in cases such as restore the selection + * after rotation etc. */ public void restoreSelection(Selection other) { - setItemsSelected(other.mSelection, true); + setItemsSelectedQuietly(other.mSelection, true); // NOTE: We intentionally don't restore provisional selection. It's provisional. + notifySelectionRestored(); } /** @@ -189,15 +204,20 @@ public final class MultiSelectManager { * @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) { - boolean itemChanged = selected ? mSelection.add(id) : mSelection.remove(id); + final boolean itemChanged = selected ? mSelection.add(id) : mSelection.remove(id); if (itemChanged) { notifyItemStateChanged(id, selected); } changed |= itemChanged; } - notifySelectionChanged(); return changed; } @@ -239,12 +259,10 @@ public final class MultiSelectManager { public void toggleSelection(String modelId) { assert(modelId != null); - boolean changed = false; - if (mSelection.contains(modelId)) { - changed = attemptDeselect(modelId); - } else { - changed = attemptSelect(modelId); - } + final boolean changed = + mSelection.contains(modelId) + ? attemptDeselect(modelId) + : attemptSelect(modelId); if (changed) { notifySelectionChanged(); @@ -345,7 +363,7 @@ public final class MultiSelectManager { */ private boolean attemptDeselect(String id) { assert(id != null); - if (notifyBeforeItemStateChange(id, false)) { + if (canSetState(id, false)) { mSelection.remove(id); notifyItemStateChanged(id, false); if (DEBUG) Log.d(TAG, "Selection after deselect: " + mSelection); @@ -362,7 +380,7 @@ public final class MultiSelectManager { */ private boolean attemptSelect(String id) { assert(id != null); - boolean canSelect = notifyBeforeItemStateChange(id, true); + boolean canSelect = canSetState(id, true); if (!canSelect) { return false; } @@ -374,14 +392,8 @@ public final class MultiSelectManager { return true; } - 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)) { - return false; - } - } - return true; + boolean canSetState(String id, boolean nextState) { + return mCanSetState.test(id, nextState); } /** @@ -390,9 +402,9 @@ public final class MultiSelectManager { */ void notifyItemStateChanged(String id, boolean selected) { assert(id != null); - int lastListener = mCallbacks.size() - 1; - for (int i = lastListener; i > -1; i--) { - mCallbacks.get(i).onItemStateChanged(id, selected); + int lastListener = mItemCallbacks.size() - 1; + for (int i = lastListener; i >= 0; i--) { + mItemCallbacks.get(i).onItemStateChanged(id, selected); } mAdapter.onItemSelectionChanged(id); } @@ -410,6 +422,13 @@ public final class MultiSelectManager { } } + private void notifySelectionRestored() { + int lastListener = mCallbacks.size() - 1; + for (int i = lastListener; i > -1; i--) { + mCallbacks.get(i).onSelectionRestored(); + } + } + private void updateForRange(int begin, int end, boolean selected, @RangeType int type) { switch (type) { case RANGE_REGULAR: @@ -432,7 +451,7 @@ public final class MultiSelectManager { } if (selected) { - boolean canSelect = notifyBeforeItemStateChange(id, true); + boolean canSelect = canSetState(id, true); if (canSelect) { if (mSingleSelect && hasSelection()) { clearSelectionQuietly(); @@ -453,7 +472,10 @@ public final class MultiSelectManager { continue; } if (selected) { - mSelection.mProvisionalSelection.add(id); + boolean canSelect = canSetState(id, true); + if (canSelect) { + mSelection.mProvisionalSelection.add(id); + } } else { mSelection.mProvisionalSelection.remove(id); } @@ -834,29 +856,24 @@ public final class MultiSelectManager { }; } + public interface ItemCallback { + void onItemStateChanged(String id, boolean selected); + } + public interface Callback { /** - * Called when an item is selected or unselected while in selection mode. - * - * @param position Adapter position of the item that was checked or unchecked - * @param selected <code>true</code> if the item is now selected, <code>false</code> - * if the item is now unselected. + * Called immediately after completion of any set of changes. */ - public void onItemStateChanged(String id, boolean selected); + void onSelectionChanged(); /** - * Called prior to an item changing state. Callbacks can cancel - * the change at {@code position} by returning {@code false}. - * - * @param id Adapter position of the item that was checked or unchecked - * @param selected <code>true</code> if the item is to be selected, <code>false</code> - * if the item is to be unselected. + * Called immediately after selection is restored. */ - public boolean onBeforeItemStateChange(String id, boolean selected); + void onSelectionRestored(); + } - /** - * Called immediately after completion of any set of changes. - */ - public void onSelectionChanged(); + @FunctionalInterface + public interface SelectionPredicate { + boolean test(String id, boolean nextState); } } diff --git a/src/com/android/documentsui/dirlist/SelectionMetadata.java b/src/com/android/documentsui/dirlist/SelectionMetadata.java new file mode 100644 index 000000000..6f917495f --- /dev/null +++ b/src/com/android/documentsui/dirlist/SelectionMetadata.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.documentsui.dirlist; + +import static com.android.documentsui.model.DocumentInfo.getCursorInt; +import static com.android.documentsui.model.DocumentInfo.getCursorString; + +import android.database.Cursor; +import android.provider.DocumentsContract.Document; +import android.util.Log; + +import com.android.documentsui.MenuManager; +import com.android.documentsui.MimePredicate; + +import java.util.function.Function; + +/** + * A class that holds metadata + */ +class SelectionMetadata implements MenuManager.SelectionDetails, MultiSelectManager.ItemCallback { + + private static final String TAG = "SelectionMetadata"; + + private final MultiSelectManager mSelectionMgr; + private final Function<String, Cursor> mDocFinder; + + // Partial files are files that haven't been fully downloaded. + private int mPartialCount = 0; + private int mDirectoryCount = 0; + private int mWritableDirectoryCount = 0; + private int mNoDeleteCount = 0; + private int mNoRenameCount = 0; + + SelectionMetadata( + MultiSelectManager selectionMgr, Function<String, Cursor> docFinder) { + mSelectionMgr = selectionMgr; + mDocFinder = docFinder; + } + + @Override + public void onItemStateChanged(String modelId, boolean selected) { + final Cursor cursor = mDocFinder.apply(modelId); + if (cursor == null) { + Log.w(TAG, "Model returned null cursor for document: " + modelId + + ". Ignoring state changed event."); + return; + } + + final String mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); + if (MimePredicate.isDirectoryType(mimeType)) { + mDirectoryCount += selected ? 1 : -1; + } + + final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS); + if ((docFlags & Document.FLAG_PARTIAL) != 0) { + mPartialCount += selected ? 1 : -1; + } + if ((docFlags & Document.FLAG_DIR_SUPPORTS_CREATE) != 0) { + mWritableDirectoryCount += selected ? 1 : -1; + } + if ((docFlags & Document.FLAG_SUPPORTS_DELETE) == 0) { + mNoDeleteCount += selected ? 1 : -1; + } + if ((docFlags & Document.FLAG_SUPPORTS_RENAME) == 0) { + mNoRenameCount += selected ? 1 : -1; + } + } + + @Override + public boolean containsDirectories() { + return mDirectoryCount > 0; + } + + @Override + public boolean containsPartialFiles() { + return mPartialCount > 0; + } + + @Override + public boolean canDelete() { + return mNoDeleteCount == 0; + } + + @Override + public boolean canRename() { + return mNoRenameCount == 0 && mSelectionMgr.getSelection().size() == 1; + } + + @Override + public boolean canPasteInto() { + return mDirectoryCount == 1 && mWritableDirectoryCount == 1 + && mSelectionMgr.getSelection().size() == 1; + } +} diff --git a/src/com/android/documentsui/dirlist/UserInputHandler.java b/src/com/android/documentsui/dirlist/UserInputHandler.java index bf6ac3cd7..604ccd6c8 100644 --- a/src/com/android/documentsui/dirlist/UserInputHandler.java +++ b/src/com/android/documentsui/dirlist/UserInputHandler.java @@ -25,8 +25,8 @@ import android.view.KeyEvent; import android.view.MotionEvent; import com.android.documentsui.Events; -import com.android.documentsui.Events.EventHandler; import com.android.documentsui.Events.InputEvent; +import com.android.documentsui.base.FunctionalInterfaces.EventHandler; import java.util.Collections; import java.util.function.Function; @@ -47,11 +47,11 @@ public final class UserInputHandler<T extends InputEvent> private final FocusHandler mFocusHandler; private final Function<MotionEvent, T> mEventConverter; private final Predicate<DocumentDetails> mSelectable; - private final EventHandler mRightClickHandler; + private final EventHandler<InputEvent> mRightClickHandler; private final DocumentHandler mActivateHandler; private final DocumentHandler mDeleteHandler; - private final EventHandler mTouchDragListener; - private final EventHandler mGestureSelectHandler; + private final EventHandler<InputEvent> mTouchDragListener; + private final EventHandler<InputEvent> mGestureSelectHandler; private final TouchInputDelegate mTouchDelegate; private final MouseInputDelegate mMouseDelegate; private final KeyInputHandler mKeyListener; @@ -61,11 +61,11 @@ public final class UserInputHandler<T extends InputEvent> FocusHandler focusHandler, Function<MotionEvent, T> eventConverter, Predicate<DocumentDetails> selectable, - EventHandler rightClickHandler, + EventHandler<InputEvent> rightClickHandler, DocumentHandler activateHandler, DocumentHandler deleteHandler, - EventHandler touchDragListener, - EventHandler gestureSelectHandler) { + EventHandler<InputEvent> touchDragListener, + EventHandler<InputEvent> gestureSelectHandler) { mSelectionMgr = selectionMgr; mFocusHandler = focusHandler; diff --git a/tests/src/com/android/documentsui/dirlist/DragStartListenerTest.java b/tests/src/com/android/documentsui/dirlist/DragStartListenerTest.java index d51ef1f3b..81612a942 100644 --- a/tests/src/com/android/documentsui/dirlist/DragStartListenerTest.java +++ b/tests/src/com/android/documentsui/dirlist/DragStartListenerTest.java @@ -26,6 +26,7 @@ import com.android.documentsui.Events.InputEvent; import com.android.documentsui.State; import com.android.documentsui.dirlist.MultiSelectManager.Selection; import com.android.documentsui.testing.TestEvent; +import com.android.documentsui.testing.MultiSelectManagers; import com.android.documentsui.testing.Views; import java.util.ArrayList; @@ -41,10 +42,7 @@ public class DragStartListenerTest extends AndroidTestCase { @Override public void setUp() throws Exception { - - mMultiSelectManager = new MultiSelectManager( - new TestDocumentsAdapter(new ArrayList<String>()), - MultiSelectManager.MODE_MULTIPLE); + mMultiSelectManager = MultiSelectManagers.createTestInstance(); mListener = new DragStartListener.ActiveListener( new State(), diff --git a/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java b/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java index 237899bf6..1c7b863d3 100644 --- a/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java +++ b/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java @@ -16,38 +16,49 @@ package com.android.documentsui.dirlist; -import android.test.AndroidTestCase; -import android.test.suitebuilder.annotation.SmallTest; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; import android.util.SparseBooleanArray; import com.android.documentsui.dirlist.MultiSelectManager.Selection; +import com.android.documentsui.testing.MultiSelectManagers; import com.android.documentsui.testing.dirlist.SelectionProbe; import com.android.documentsui.testing.dirlist.TestSelectionListener; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + import java.util.HashSet; import java.util.List; import java.util.Set; +@RunWith(AndroidJUnit4.class) @SmallTest -public class MultiSelectManagerTest extends AndroidTestCase { +public class MultiSelectManagerTest { private static final List<String> ITEMS = TestData.create(100); + private final Set<String> mIgnored = new HashSet<>(); private MultiSelectManager mManager; private TestSelectionListener mCallback; - private TestDocumentsAdapter mAdapter; private SelectionProbe mSelection; - @Override + @Before public void setUp() throws Exception { mCallback = new TestSelectionListener(); - mAdapter = new TestDocumentsAdapter(ITEMS); - mManager = new MultiSelectManager(mAdapter, MultiSelectManager.MODE_MULTIPLE); + mManager = MultiSelectManagers.createTestInstance( + ITEMS, + MultiSelectManager.MODE_MULTIPLE, + (String id, boolean nextState) -> (!nextState || !mIgnored.contains(id))); mManager.addCallback(mCallback); mSelection = new SelectionProbe(mManager); + + mIgnored.clear(); } + @Test public void testSelection() { // Check selection. mManager.toggleSelection(ITEMS.get(7)); @@ -57,6 +68,15 @@ public class MultiSelectManagerTest extends AndroidTestCase { mSelection.assertNoSelection(); } + @Test + public void testSelection_DoNothingOnUnselectableItem() { + mIgnored.add(ITEMS.get(7)); + + mManager.toggleSelection(ITEMS.get(7)); + mSelection.assertNoSelection(); + } + + @Test public void testSelection_NotifiesSelectionChanged() { // Selection should notify. mManager.toggleSelection(ITEMS.get(7)); @@ -66,12 +86,26 @@ public class MultiSelectManagerTest extends AndroidTestCase { mCallback.assertSelectionChanged(); } + @Test public void testRangeSelection() { mManager.startRangeSelection(15); mManager.snapRangeSelection(19); mSelection.assertRangeSelection(15, 19); } + @Test + public void testRangeSelection_SkipUnselectableItem() { + mIgnored.add(ITEMS.get(17)); + + mManager.startRangeSelection(15); + mManager.snapRangeSelection(19); + + mSelection.assertRangeSelected(15, 16); + mSelection.assertNotSelected(17); + mSelection.assertRangeSelected(18, 19); + } + + @Test public void testRangeSelection_snapExpand() { mManager.startRangeSelection(15); mManager.snapRangeSelection(19); @@ -79,6 +113,7 @@ public class MultiSelectManagerTest extends AndroidTestCase { mSelection.assertRangeSelection(15, 27); } + @Test public void testRangeSelection_snapContract() { mManager.startRangeSelection(15); mManager.snapRangeSelection(27); @@ -86,6 +121,7 @@ public class MultiSelectManagerTest extends AndroidTestCase { mSelection.assertRangeSelection(15, 19); } + @Test public void testRangeSelection_snapInvert() { mManager.startRangeSelection(15); mManager.snapRangeSelection(27); @@ -93,6 +129,7 @@ public class MultiSelectManagerTest extends AndroidTestCase { mSelection.assertRangeSelection(3, 15); } + @Test public void testRangeSelection_multiple() { mManager.startRangeSelection(15); mManager.snapRangeSelection(27); @@ -104,6 +141,7 @@ public class MultiSelectManagerTest extends AndroidTestCase { mSelection.assertRangeSelected(42, 57); } + @Test public void testProvisionalRangeSelection() { mManager.startRangeSelection(13); mManager.snapProvisionalRangeSelection(15); @@ -113,6 +151,7 @@ public class MultiSelectManagerTest extends AndroidTestCase { mSelection.assertSelectionSize(3); } + @Test public void testProvisionalRangeSelection_endEarly() { mManager.startRangeSelection(13); mManager.snapProvisionalRangeSelection(15); @@ -124,6 +163,7 @@ public class MultiSelectManagerTest extends AndroidTestCase { mSelection.assertSelectionSize(1); } + @Test public void testProvisionalRangeSelection_snapExpand() { mManager.startRangeSelection(13); mManager.snapProvisionalRangeSelection(15); @@ -133,6 +173,7 @@ public class MultiSelectManagerTest extends AndroidTestCase { mSelection.assertRangeSelection(13, 18); } + @Test public void testCombinationRangeSelection_IntersectsOldSelection() { mManager.startRangeSelection(13); mManager.snapRangeSelection(15); @@ -148,6 +189,7 @@ public class MultiSelectManagerTest extends AndroidTestCase { mSelection.assertSelectionSize(4); } + @Test public void testProvisionalSelection() { Selection s = mManager.getSelection(); mSelection.assertNoSelection(); @@ -159,6 +201,7 @@ public class MultiSelectManagerTest extends AndroidTestCase { mSelection.assertSelection(1, 2); } + @Test public void testProvisionalSelection_Replace() { Selection s = mManager.getSelection(); @@ -174,6 +217,7 @@ public class MultiSelectManagerTest extends AndroidTestCase { mSelection.assertSelection(3, 4); } + @Test public void testProvisionalSelection_IntersectsExistingProvisionalSelection() { Selection s = mManager.getSelection(); @@ -188,6 +232,7 @@ public class MultiSelectManagerTest extends AndroidTestCase { mSelection.assertSelection(1); } + @Test public void testProvisionalSelection_Apply() { Selection s = mManager.getSelection(); @@ -199,6 +244,7 @@ public class MultiSelectManagerTest extends AndroidTestCase { mSelection.assertSelection(1, 2); } + @Test public void testProvisionalSelection_Cancel() { mManager.toggleSelection(ITEMS.get(1)); mManager.toggleSelection(ITEMS.get(2)); @@ -214,6 +260,7 @@ public class MultiSelectManagerTest extends AndroidTestCase { mSelection.assertSelection(1, 2); } + @Test public void testProvisionalSelection_IntersectsAppliedSelection() { mManager.toggleSelection(ITEMS.get(1)); mManager.toggleSelection(ITEMS.get(2)); diff --git a/tests/src/com/android/documentsui/dirlist/MultiSelectManager_SelectionTest.java b/tests/src/com/android/documentsui/dirlist/MultiSelectManager_SelectionTest.java index 444b2dc8e..11818ac1c 100644 --- a/tests/src/com/android/documentsui/dirlist/MultiSelectManager_SelectionTest.java +++ b/tests/src/com/android/documentsui/dirlist/MultiSelectManager_SelectionTest.java @@ -16,17 +16,27 @@ package com.android.documentsui.dirlist; -import android.test.AndroidTestCase; -import android.test.suitebuilder.annotation.SmallTest; +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; import com.android.documentsui.dirlist.MultiSelectManager.Selection; + import com.google.common.collect.Sets; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + import java.util.HashSet; import java.util.Set; +@RunWith(AndroidJUnit4.class) @SmallTest -public class MultiSelectManager_SelectionTest extends AndroidTestCase { +public class MultiSelectManager_SelectionTest { private Selection selection; @@ -36,7 +46,7 @@ public class MultiSelectManager_SelectionTest extends AndroidTestCase { "auth|id=@53di*/f3#d" }; - @Override + @Before public void setUp() throws Exception { selection = new Selection(); selection.add(ids[0]); @@ -44,6 +54,7 @@ public class MultiSelectManager_SelectionTest extends AndroidTestCase { selection.add(ids[2]); } + @Test public void testAdd() { // We added in setUp. assertEquals(3, selection.size()); @@ -52,6 +63,7 @@ public class MultiSelectManager_SelectionTest extends AndroidTestCase { assertContains(ids[2]); } + @Test public void testRemove() { selection.remove(ids[0]); selection.remove(ids[2]); @@ -59,11 +71,13 @@ public class MultiSelectManager_SelectionTest extends AndroidTestCase { assertContains(ids[1]); } + @Test public void testClear() { selection.clear(); assertEquals(0, selection.size()); } + @Test public void testIsEmpty() { assertTrue(new Selection().isEmpty()); selection.clear(); diff --git a/tests/src/com/android/documentsui/dirlist/MultiSelectManager_SingleSelectTest.java b/tests/src/com/android/documentsui/dirlist/MultiSelectManager_SingleSelectTest.java index 62cb1b05f..020f316f6 100644 --- a/tests/src/com/android/documentsui/dirlist/MultiSelectManager_SingleSelectTest.java +++ b/tests/src/com/android/documentsui/dirlist/MultiSelectManager_SingleSelectTest.java @@ -16,16 +16,24 @@ package com.android.documentsui.dirlist; -import android.test.AndroidTestCase; -import android.test.suitebuilder.annotation.SmallTest; +import static junit.framework.Assert.fail; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import com.android.documentsui.testing.MultiSelectManagers; import com.android.documentsui.testing.dirlist.SelectionProbe; import com.android.documentsui.testing.dirlist.TestSelectionListener; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + import java.util.List; +@RunWith(AndroidJUnit4.class) @SmallTest -public class MultiSelectManager_SingleSelectTest extends AndroidTestCase { +public class MultiSelectManager_SingleSelectTest { private static final List<String> ITEMS = TestData.create(100); @@ -34,16 +42,16 @@ public class MultiSelectManager_SingleSelectTest extends AndroidTestCase { private TestDocumentsAdapter mAdapter; private SelectionProbe mSelection; - @Override + @Before public void setUp() throws Exception { mCallback = new TestSelectionListener(); - mAdapter = new TestDocumentsAdapter(ITEMS); - mManager = new MultiSelectManager(mAdapter, MultiSelectManager.MODE_SINGLE); + mManager = MultiSelectManagers.createTestInstance(ITEMS, MultiSelectManager.MODE_SINGLE); mManager.addCallback(mCallback); mSelection = new SelectionProbe(mManager); } + @Test public void testSimpleSelect() { mManager.toggleSelection(ITEMS.get(3)); mManager.toggleSelection(ITEMS.get(4)); @@ -51,6 +59,7 @@ public class MultiSelectManager_SingleSelectTest extends AndroidTestCase { mSelection.assertSelection(4); } + @Test public void testRangeSelectionNotEstablished() { mManager.toggleSelection(ITEMS.get(3)); mCallback.reset(); diff --git a/tests/src/com/android/documentsui/dirlist/UserInputHandler_MouseTest.java b/tests/src/com/android/documentsui/dirlist/UserInputHandler_MouseTest.java index ef8fd9fcd..0d99bab8b 100644 --- a/tests/src/com/android/documentsui/dirlist/UserInputHandler_MouseTest.java +++ b/tests/src/com/android/documentsui/dirlist/UserInputHandler_MouseTest.java @@ -24,6 +24,7 @@ import android.support.v7.widget.RecyclerView; import android.view.MotionEvent; import com.android.documentsui.Events.InputEvent; +import com.android.documentsui.testing.MultiSelectManagers; import com.android.documentsui.testing.TestEvent; import com.android.documentsui.testing.TestEvent.Builder; import com.android.documentsui.testing.TestPredicate; @@ -57,9 +58,7 @@ public final class UserInputHandler_MouseTest { @Before public void setUp() { - mAdapter = new TestDocumentsAdapter(ITEMS); - MultiSelectManager selectionMgr = - new MultiSelectManager(mAdapter, MultiSelectManager.MODE_MULTIPLE); + MultiSelectManager selectionMgr = MultiSelectManagers.createTestInstance(ITEMS); mSelection = new SelectionProbe(selectionMgr); mCanSelect = new TestPredicate<>(); diff --git a/tests/src/com/android/documentsui/dirlist/UserInputHandler_RangeTest.java b/tests/src/com/android/documentsui/dirlist/UserInputHandler_RangeTest.java index 1223e3a57..6500a562d 100644 --- a/tests/src/com/android/documentsui/dirlist/UserInputHandler_RangeTest.java +++ b/tests/src/com/android/documentsui/dirlist/UserInputHandler_RangeTest.java @@ -21,6 +21,7 @@ import android.support.test.runner.AndroidJUnit4; import android.view.MotionEvent; import com.android.documentsui.Events.InputEvent; +import com.android.documentsui.testing.MultiSelectManagers; import com.android.documentsui.testing.TestEvent; import com.android.documentsui.testing.TestEvent.Builder; import com.android.documentsui.testing.TestPredicate; @@ -57,9 +58,7 @@ public final class UserInputHandler_RangeTest { @Before public void setUp() { - mAdapter = new TestDocumentsAdapter(ITEMS); - MultiSelectManager selectionMgr = - new MultiSelectManager(mAdapter, MultiSelectManager.MODE_MULTIPLE); + MultiSelectManager selectionMgr = MultiSelectManagers.createTestInstance(ITEMS); mSelection = new SelectionProbe(selectionMgr); mCanSelect = new TestPredicate<>(); diff --git a/tests/src/com/android/documentsui/dirlist/UserInputHandler_TouchTest.java b/tests/src/com/android/documentsui/dirlist/UserInputHandler_TouchTest.java index 11c422215..3946fd665 100644 --- a/tests/src/com/android/documentsui/dirlist/UserInputHandler_TouchTest.java +++ b/tests/src/com/android/documentsui/dirlist/UserInputHandler_TouchTest.java @@ -24,6 +24,7 @@ import android.support.v7.widget.RecyclerView; import android.view.MotionEvent; import com.android.documentsui.Events.InputEvent; +import com.android.documentsui.testing.MultiSelectManagers; import com.android.documentsui.testing.TestEvent; import com.android.documentsui.testing.TestEvent.Builder; import com.android.documentsui.testing.TestPredicate; @@ -56,10 +57,7 @@ public final class UserInputHandler_TouchTest { @Before public void setUp() { - - mAdapter = new TestDocumentsAdapter(ITEMS); - MultiSelectManager selectionMgr = - new MultiSelectManager(mAdapter, MultiSelectManager.MODE_MULTIPLE); + MultiSelectManager selectionMgr = MultiSelectManagers.createTestInstance(ITEMS); mSelection = new SelectionProbe(selectionMgr); mCanSelect = new TestPredicate<>(); diff --git a/tests/src/com/android/documentsui/testing/MultiSelectManagers.java b/tests/src/com/android/documentsui/testing/MultiSelectManagers.java new file mode 100644 index 000000000..e3b426133 --- /dev/null +++ b/tests/src/com/android/documentsui/testing/MultiSelectManagers.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.documentsui.testing; + +import com.android.documentsui.dirlist.MultiSelectManager; +import com.android.documentsui.dirlist.MultiSelectManager.SelectionMode; +import com.android.documentsui.dirlist.MultiSelectManager.SelectionPredicate; +import com.android.documentsui.dirlist.TestDocumentsAdapter; + +import java.util.Collections; +import java.util.List; + +public class MultiSelectManagers { + private MultiSelectManagers() {} + + public static MultiSelectManager createTestInstance() { + return createTestInstance(Collections.emptyList()); + } + + public static MultiSelectManager createTestInstance(List<String> docs) { + return createTestInstance(docs, MultiSelectManager.MODE_MULTIPLE); + } + + public static MultiSelectManager createTestInstance( + List<String> docs, @SelectionMode int mode) { + return createTestInstance( + docs, + mode, + (String id, boolean nextState) -> true); + } + + public static MultiSelectManager createTestInstance( + List<String> docs, @SelectionMode int mode, SelectionPredicate canSetState) { + TestDocumentsAdapter adapter = new TestDocumentsAdapter(docs); + MultiSelectManager manager = new MultiSelectManager( + adapter, + mode, + canSetState); + + return manager; + } +} diff --git a/tests/src/com/android/documentsui/testing/dirlist/TestSelectionListener.java b/tests/src/com/android/documentsui/testing/dirlist/TestSelectionListener.java index 08f29f013..06c221927 100644 --- a/tests/src/com/android/documentsui/testing/dirlist/TestSelectionListener.java +++ b/tests/src/com/android/documentsui/testing/dirlist/TestSelectionListener.java @@ -21,27 +21,18 @@ import static org.junit.Assert.assertTrue; import com.android.documentsui.dirlist.MultiSelectManager; -import java.util.HashSet; -import java.util.Set; - public final class TestSelectionListener implements MultiSelectManager.Callback { - Set<String> ignored = new HashSet<>(); private boolean mSelectionChanged = false; @Override - public void onItemStateChanged(String modelId, boolean selected) {} - - @Override - public boolean onBeforeItemStateChange(String modelId, boolean selected) { - return !ignored.contains(modelId); - } - - @Override public void onSelectionChanged() { mSelectionChanged = true; } + @Override + public void onSelectionRestored() {} + public void reset() { mSelectionChanged = false; } |