Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8250db41 authored by Ben Kwa's avatar Ben Kwa
Browse files

Add shift-selection to DocumentsUI.

- Move the key listener from BaseActivity into DirectoryFragment, where it
belongs.
- Add code to detect the shift key during keyboard navigation, and
extend the selection in that case.

BUG=20859059

Change-Id: Ia7d3c7d4343f0185873deeaf1a35028a716b6e19
parent 072c5bdf
Loading
Loading
Loading
Loading
+0 −19
Original line number Original line Diff line number Diff line
@@ -466,25 +466,6 @@ abstract class BaseActivity extends Activity {
        }
        }
    }
    }


    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        if (DEBUG) Log.d(mTag, "onKeyUp: keycode = " + keyCode);

        // TODO: Support for RecentsCreateFragment.
        DirectoryFragment fragment = DirectoryFragment.get(getFragmentManager());
        if (fragment != null) {
            switch (keyCode) {
                case KeyEvent.KEYCODE_MOVE_HOME:
                    fragment.focusFirstFile();
                    return true;
                case KeyEvent.KEYCODE_MOVE_END:
                    fragment.focusLastFile();
                    return true;
            }
        }
        return super.onKeyUp(keyCode, event);
    }

    public void onStackPicked(DocumentStack stack) {
    public void onStackPicked(DocumentStack stack) {
        try {
        try {
            // Update the restored stack to ensure we have freshest data
            // Update the restored stack to ensure we have freshest data
+39 −104
Original line number Original line Diff line number Diff line
@@ -17,7 +17,6 @@
package com.android.documentsui;
package com.android.documentsui;


import static com.android.documentsui.Shared.DEBUG;
import static com.android.documentsui.Shared.DEBUG;
import static com.android.documentsui.Shared.TAG;
import static com.android.documentsui.State.ACTION_BROWSE;
import static com.android.documentsui.State.ACTION_BROWSE;
import static com.android.documentsui.State.ACTION_CREATE;
import static com.android.documentsui.State.ACTION_CREATE;
import static com.android.documentsui.State.ACTION_MANAGE;
import static com.android.documentsui.State.ACTION_MANAGE;
@@ -79,6 +78,7 @@ import android.util.TypedValue;
import android.view.ActionMode;
import android.view.ActionMode;
import android.view.DragEvent;
import android.view.DragEvent;
import android.view.GestureDetector;
import android.view.GestureDetector;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MenuItem;
@@ -97,7 +97,6 @@ import com.android.documentsui.RecentsProvider.StateColumns;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.model.RootInfo;
import com.android.documentsui.model.RootInfo;

import com.google.common.collect.Lists;
import com.google.common.collect.Lists;


import java.util.ArrayList;
import java.util.ArrayList;
@@ -109,6 +108,8 @@ import java.util.List;
 */
 */
public class DirectoryFragment extends Fragment {
public class DirectoryFragment extends Fragment {


    public static final String TAG = "DirectoryFragment";

    public static final int TYPE_NORMAL = 1;
    public static final int TYPE_NORMAL = 1;
    public static final int TYPE_SEARCH = 2;
    public static final int TYPE_SEARCH = 2;
    public static final int TYPE_RECENT_OPEN = 3;
    public static final int TYPE_RECENT_OPEN = 3;
@@ -130,6 +131,7 @@ public class DirectoryFragment extends Fragment {
    private static final String EXTRA_IGNORE_STATE = "ignoreState";
    private static final String EXTRA_IGNORE_STATE = "ignoreState";


    private Model mModel;
    private Model mModel;
    private MultiSelectManager mSelectionManager;
    private Model.UpdateListener mModelUpdateListener = new ModelUpdateListener();
    private Model.UpdateListener mModelUpdateListener = new ModelUpdateListener();


    private View mEmptyView;
    private View mEmptyView;
@@ -268,7 +270,7 @@ public class DirectoryFragment extends Fragment {
        }
        }


        // Clear any outstanding selection
        // Clear any outstanding selection
        mModel.clearSelection();
        mSelectionManager.clearSelection();
    }
    }


    @Override
    @Override
@@ -299,16 +301,16 @@ public class DirectoryFragment extends Fragment {


        // TODO: instead of inserting the view into the constructor, extract listener-creation code
        // TODO: instead of inserting the view into the constructor, extract listener-creation code
        // and set the listener on the view after the fact.  Then the view doesn't need to be passed
        // and set the listener on the view after the fact.  Then the view doesn't need to be passed
        // into the selection manager which is passed into the model.
        // into the selection manager.
        MultiSelectManager selMgr= new MultiSelectManager(
        mSelectionManager = new MultiSelectManager(
                mRecView,
                mRecView,
                listener,
                listener,
                state.allowMultiple
                state.allowMultiple
                    ? MultiSelectManager.MODE_MULTIPLE
                    ? MultiSelectManager.MODE_MULTIPLE
                    : MultiSelectManager.MODE_SINGLE);
                    : MultiSelectManager.MODE_SINGLE);
        selMgr.addCallback(new SelectionModeListener());
        mSelectionManager.addCallback(new SelectionModeListener());


        mModel = new Model(context, selMgr, mAdapter);
        mModel = new Model(context, mAdapter);
        mModel.addUpdateListener(mModelUpdateListener);
        mModel.addUpdateListener(mModelUpdateListener);


        mType = getArguments().getInt(EXTRA_TYPE);
        mType = getArguments().getInt(EXTRA_TYPE);
@@ -430,7 +432,7 @@ public class DirectoryFragment extends Fragment {
    }
    }


    private boolean onSingleTapUp(MotionEvent e) {
    private boolean onSingleTapUp(MotionEvent e) {
        if (Events.isTouchEvent(e) && mModel.getSelection().isEmpty()) {
        if (Events.isTouchEvent(e) && mSelectionManager.getSelection().isEmpty()) {
            int position = getEventAdapterPosition(e);
            int position = getEventAdapterPosition(e);
            if (position != RecyclerView.NO_POSITION) {
            if (position != RecyclerView.NO_POSITION) {
                return handleViewItem(position);
                return handleViewItem(position);
@@ -458,7 +460,7 @@ public class DirectoryFragment extends Fragment {
        if (isDocumentEnabled(docMimeType, docFlags)) {
        if (isDocumentEnabled(docMimeType, docFlags)) {
            final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
            final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
            ((BaseActivity) getActivity()).onDocumentPicked(doc, mModel);
            ((BaseActivity) getActivity()).onDocumentPicked(doc, mModel);
            mModel.clearSelection();
            mSelectionManager.clearSelection();
            return true;
            return true;
        }
        }
        return false;
        return false;
@@ -564,7 +566,7 @@ public class DirectoryFragment extends Fragment {
        mRecView.setLayoutManager(layout);
        mRecView.setLayoutManager(layout);
        // TODO: Once b/23691541 is resolved, use a listener within MultiSelectManager instead of
        // TODO: Once b/23691541 is resolved, use a listener within MultiSelectManager instead of
        // imperatively calling this function.
        // imperatively calling this function.
        mModel.mSelectionManager.handleLayoutChanged();
        mSelectionManager.handleLayoutChanged();
        // setting layout manager automatically invalidates existing ViewHolders.
        // setting layout manager automatically invalidates existing ViewHolders.
        mThumbSize = new Point(thumbSize, thumbSize);
        mThumbSize = new Point(thumbSize, thumbSize);
    }
    }
@@ -620,7 +622,7 @@ public class DirectoryFragment extends Fragment {


        @Override
        @Override
        public void onSelectionChanged() {
        public void onSelectionChanged() {
            mModel.getSelection(mSelected);
            mSelectionManager.getSelection(mSelected);
            TypedValue color = new TypedValue();
            TypedValue color = new TypedValue();
            if (mSelected.size() > 0) {
            if (mSelected.size() > 0) {
                if (DEBUG) Log.d(TAG, "Maybe starting action mode.");
                if (DEBUG) Log.d(TAG, "Maybe starting action mode.");
@@ -628,8 +630,7 @@ public class DirectoryFragment extends Fragment {
                    if (DEBUG) Log.d(TAG, "Yeah. Starting action mode.");
                    if (DEBUG) Log.d(TAG, "Yeah. Starting action mode.");
                    mActionMode = getActivity().startActionMode(this);
                    mActionMode = getActivity().startActionMode(this);
                }
                }
                getActivity().getTheme().resolveAttribute(
                getActivity().getTheme().resolveAttribute(R.attr.colorActionMode, color, true);
                    R.attr.colorActionMode, color, true);
                updateActionMenu();
                updateActionMenu();
            } else {
            } else {
                if (DEBUG) Log.d(TAG, "Finishing action mode.");
                if (DEBUG) Log.d(TAG, "Finishing action mode.");
@@ -652,16 +653,17 @@ public class DirectoryFragment extends Fragment {
            if (DEBUG) Log.d(TAG, "Handling action mode destroyed.");
            if (DEBUG) Log.d(TAG, "Handling action mode destroyed.");
            mActionMode = null;
            mActionMode = null;
            // clear selection
            // clear selection
            mModel.clearSelection();
            mSelectionManager.clearSelection();
            mSelected.clear();
            mSelected.clear();
            mNoDeleteCount = 0;
            mNoDeleteCount = 0;
        }
        }


        @Override
        @Override
        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
            int size = mSelectionManager.getSelection().size();
            mode.getMenuInflater().inflate(R.menu.mode_directory, menu);
            mode.getMenuInflater().inflate(R.menu.mode_directory, menu);
            mode.setTitle(TextUtils.formatSelectedCount(mModel.getSelection().size()));
            mode.setTitle(TextUtils.formatSelectedCount(size));
            return mModel.getSelection().size() > 0;
            return (size > 0);
        }
        }


        @Override
        @Override
@@ -681,7 +683,7 @@ public class DirectoryFragment extends Fragment {
        @Override
        @Override
        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {


            Selection selection = mModel.getSelection(new Selection());
            Selection selection = mSelectionManager.getSelection(new Selection());


            final int id = item.getItemId();
            final int id = item.getItemId();
            if (id == R.id.menu_open) {
            if (id == R.id.menu_open) {
@@ -920,15 +922,22 @@ public class DirectoryFragment extends Fragment {
        public DocumentHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        public DocumentHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            final State state = getDisplayState(DirectoryFragment.this);
            final State state = getDisplayState(DirectoryFragment.this);
            final LayoutInflater inflater = LayoutInflater.from(getContext());
            final LayoutInflater inflater = LayoutInflater.from(getContext());
            View item = null;
            switch (state.derivedMode) {
            switch (state.derivedMode) {
                case MODE_GRID:
                case MODE_GRID:
                    return new DocumentHolder(inflater.inflate(R.layout.item_doc_grid, parent, false));
                    item = inflater.inflate(R.layout.item_doc_grid, parent, false);
                    break;
                case MODE_LIST:
                case MODE_LIST:
                    return new DocumentHolder(inflater.inflate(R.layout.item_doc_list, parent, false));
                    item = inflater.inflate(R.layout.item_doc_list, parent, false);
                    break;
                case MODE_UNKNOWN:
                case MODE_UNKNOWN:
                default:
                default:
                    throw new IllegalStateException("Unsupported layout mode.");
                    throw new IllegalStateException("Unsupported layout mode.");
            }
            }
            // Key event bubbling doesn't work properly, so instead of setting one key listener on
            // the RecyclerView, we have to set it on each Item.  See b/24865023.
            item.setOnKeyListener(mSelectionManager);
            return new DocumentHolder(item);
        }
        }


        @Override
        @Override
@@ -957,7 +966,7 @@ public class DirectoryFragment extends Fragment {


            holder.docId = docId;
            holder.docId = docId;
            final View itemView = holder.view;
            final View itemView = holder.view;
            itemView.setActivated(mModel.isSelected(position));
            itemView.setActivated(isSelected(position));


            final View line1 = itemView.findViewById(R.id.line1);
            final View line1 = itemView.findViewById(R.id.line1);
            final View line2 = itemView.findViewById(R.id.line2);
            final View line2 = itemView.findViewById(R.id.line2);
@@ -1289,7 +1298,7 @@ public class DirectoryFragment extends Fragment {
    }
    }


    void copySelectedToClipboard() {
    void copySelectedToClipboard() {
        Selection sel = mModel.getSelection(new Selection());
        Selection sel = mSelectionManager.getSelection(new Selection());
        copySelectionToClipboard(sel);
        copySelectionToClipboard(sel);
    }
    }


@@ -1338,51 +1347,12 @@ public class DirectoryFragment extends Fragment {
    }
    }


    void selectAllFiles() {
    void selectAllFiles() {
        boolean changed = mModel.selectAll();
        boolean changed = mSelectionManager.setItemsSelected(0, mModel.getItemCount(), true);
        if (changed) {
        if (changed) {
            updateDisplayState();
            updateDisplayState();
        }
        }
    }
    }


    /**
     * Scrolls to the top of the file list and focuses the first file.
     */
    void focusFirstFile() {
        focusFile(0);
    }

    /**
     * Scrolls to the bottom of the file list and focuses the last file.
     */
    void focusLastFile() {
        focusFile(mAdapter.getItemCount() - 1);
    }

    /**
     * Scrolls to and then focuses on the file at the given position.
     */
    private void focusFile(final int pos) {
        // Don't smooth scroll; that taxes the system unnecessarily and makes the scroll handling
        // logic below more complicated.
        mRecView.scrollToPosition(pos);

        // If the item is already in view, focus it; otherwise, set a one-time listener to focus it
        // when the scroll is completed.
        RecyclerView.ViewHolder vh = mRecView.findViewHolderForAdapterPosition(pos);
        if (vh != null) {
            vh.itemView.requestFocus();
        } else {
            mRecView.addOnScrollListener(
                    new RecyclerView.OnScrollListener() {
                        @Override
                        public void onScrolled(RecyclerView view, int dx, int dy) {
                            view.findViewHolderForAdapterPosition(pos).itemView.requestFocus();
                            view.removeOnScrollListener(this);
                        }
                    });
        }
    }

    private void setupDragAndDropOnDirectoryView(View view) {
    private void setupDragAndDropOnDirectoryView(View view) {
        // Listen for drops on non-directory items and empty space.
        // Listen for drops on non-directory items and empty space.
        view.setOnDragListener(mOnDragListener);
        view.setOnDragListener(mOnDragListener);
@@ -1472,9 +1442,10 @@ public class DirectoryFragment extends Fragment {
            return Collections.EMPTY_LIST;
            return Collections.EMPTY_LIST;
        }
        }


        final List<DocumentInfo> selectedDocs = mModel.getSelectedDocuments();
        final List<DocumentInfo> selectedDocs =
                mModel.getDocuments(mSelectionManager.getSelection());
        if (!selectedDocs.isEmpty()) {
        if (!selectedDocs.isEmpty()) {
            if (!mModel.isSelected(position)) {
            if (!isSelected(position)) {
                // There is a selection that does not include the current item, drag nothing.
                // There is a selection that does not include the current item, drag nothing.
                return Collections.EMPTY_LIST;
                return Collections.EMPTY_LIST;
            }
            }
@@ -1694,12 +1665,15 @@ public class DirectoryFragment extends Fragment {
        public void afterActivityCreated(DirectoryFragment fragment) {}
        public void afterActivityCreated(DirectoryFragment fragment) {}
    }
    }


    boolean isSelected(int position) {
        return mSelectionManager.getSelection().contains(position);
    }

    /**
    /**
     * The data model for the current loaded directory.
     * The data model for the current loaded directory.
     */
     */
    @VisibleForTesting
    @VisibleForTesting
    public static final class Model implements DocumentContext {
    public static final class Model implements DocumentContext {
        private MultiSelectManager mSelectionManager;
        private RecyclerView.Adapter<?> mViewAdapter;
        private RecyclerView.Adapter<?> mViewAdapter;
        private Context mContext;
        private Context mContext;
        private int mCursorCount;
        private int mCursorCount;
@@ -1710,45 +1684,11 @@ public class DirectoryFragment extends Fragment {
        @Nullable private String info;
        @Nullable private String info;
        @Nullable private String error;
        @Nullable private String error;


        Model(Context context, MultiSelectManager selectionManager,
        Model(Context context, RecyclerView.Adapter<?> viewAdapter) {
                RecyclerView.Adapter<?> viewAdapter) {
            mContext = context;
            mContext = context;
            mSelectionManager = selectionManager;
            mViewAdapter = viewAdapter;
            mViewAdapter = viewAdapter;
        }
        }


        /**
         * Selects all files in the current directory.
         * @return true if the selection state changed for any files.
         */
        boolean selectAll() {
            return mSelectionManager.setItemsSelected(0, mCursorCount, true);
        }

        /**
         * Clones the current selection into the given Selection object.
         * @param selection
         * @return The selection that was passed in, for convenience.
         */
        Selection getSelection(Selection selection) {
            return mSelectionManager.getSelection(selection);
        }

        /**
         * @return The current selection (the live instance, not a copy).
         */
        Selection getSelection() {
            return mSelectionManager.getSelection();
        }

        boolean isSelected(int position) {
            return mSelectionManager.getSelection().contains(position);
        }

        void clearSelection() {
            mSelectionManager.clearSelection();
        }

        void update(DirectoryResult result) {
        void update(DirectoryResult result) {
            if (DEBUG) Log.i(TAG, "Updating model with new result set.");
            if (DEBUG) Log.i(TAG, "Updating model with new result set.");


@@ -1821,11 +1761,6 @@ public class DirectoryFragment extends Fragment {
            return mIsLoading;
            return mIsLoading;
        }
        }


        private List<DocumentInfo> getSelectedDocuments() {
            Selection sel = getSelection(new Selection());
            return getDocuments(sel);
        }

        List<DocumentInfo> getDocuments(Selection items) {
        List<DocumentInfo> getDocuments(Selection items) {
            final int size = (items != null) ? items.size() : 0;
            final int size = (items != null) ? items.size() : 0;


+116 −15
Original line number Original line Diff line number Diff line
@@ -39,6 +39,7 @@ import android.util.SparseIntArray;
import android.view.GestureDetector;
import android.view.GestureDetector;
import android.view.GestureDetector.OnDoubleTapListener;
import android.view.GestureDetector.OnDoubleTapListener;
import android.view.GestureDetector.OnGestureListener;
import android.view.GestureDetector.OnGestureListener;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.View;


@@ -56,7 +57,7 @@ import java.util.List;
 * Additionally it can be configured to restrict selection to a single element, @see
 * Additionally it can be configured to restrict selection to a single element, @see
 * #setSelectMode.
 * #setSelectMode.
 */
 */
public final class MultiSelectManager {
public final class MultiSelectManager implements View.OnKeyListener {


    /** Selection mode for multiple select. **/
    /** Selection mode for multiple select. **/
    public static final int MODE_MULTIPLE = 0;
    public static final int MODE_MULTIPLE = 0;
@@ -72,6 +73,8 @@ public final class MultiSelectManager {
    private Selection mIntermediateSelection;
    private Selection mIntermediateSelection;


    private Range mRanger;
    private Range mRanger;
    private SelectionEnvironment mEnvironment;

    private final List<MultiSelectManager.Callback> mCallbacks = new ArrayList<>(1);
    private final List<MultiSelectManager.Callback> mCallbacks = new ArrayList<>(1);


    private Adapter<?> mAdapter;
    private Adapter<?> mAdapter;
@@ -95,10 +98,10 @@ public final class MultiSelectManager {
                new RuntimeItemFinder(recyclerView),
                new RuntimeItemFinder(recyclerView),
                mode);
                mode);


        mEnvironment = new RuntimeSelectionEnvironment(recyclerView);

        if (mode == MODE_MULTIPLE) {
        if (mode == MODE_MULTIPLE) {
            mBandManager = new BandController(
            mBandManager = new BandController(mHelper);
                    mHelper,
                    new RuntimeBandEnvironment(recyclerView));
        }
        }


        GestureDetector.SimpleOnGestureListener listener =
        GestureDetector.SimpleOnGestureListener listener =
@@ -900,7 +903,7 @@ public final class MultiSelectManager {
     * Provides functionality for BandController. Exists primarily to tests that are
     * Provides functionality for BandController. Exists primarily to tests that are
     * fully isolated from RecyclerView.
     * fully isolated from RecyclerView.
     */
     */
    interface BandEnvironment {
    interface SelectionEnvironment {
        void showBand(Rect rect);
        void showBand(Rect rect);
        void hideBand();
        void hideBand();
        void addOnScrollListener(RecyclerView.OnScrollListener listener);
        void addOnScrollListener(RecyclerView.OnScrollListener listener);
@@ -913,29 +916,39 @@ public final class MultiSelectManager {
        Point createAbsolutePoint(Point relativePoint);
        Point createAbsolutePoint(Point relativePoint);
        Rect getAbsoluteRectForChildViewAt(int index);
        Rect getAbsoluteRectForChildViewAt(int index);
        int getAdapterPositionAt(int index);
        int getAdapterPositionAt(int index);
        int getAdapterPositionForChildView(View view);
        int getColumnCount();
        int getColumnCount();
        int getRowCount();
        int getRowCount();
        int getChildCount();
        int getChildCount();
        int getVisibleChildCount();
        int getVisibleChildCount();
        void focusItem(int position);
    }
    }


    /** RvFacade implementation backed by good ol' RecyclerView. */
    /** RvFacade implementation backed by good ol' RecyclerView. */
    private static final class RuntimeBandEnvironment implements BandEnvironment {
    private static final class RuntimeSelectionEnvironment implements SelectionEnvironment {


        private final RecyclerView mView;
        private final RecyclerView mView;
        private final Drawable mBand;
        private final Drawable mBand;


        private boolean mIsOverlayShown = false;
        private boolean mIsOverlayShown = false;


        RuntimeBandEnvironment(RecyclerView rv) {
        RuntimeSelectionEnvironment(RecyclerView rv) {
            mView = rv;
            mView = rv;
            mBand = mView.getContext().getTheme().getDrawable(R.drawable.band_select_overlay);
            mBand = mView.getContext().getTheme().getDrawable(R.drawable.band_select_overlay);
        }
        }


        @Override
        public int getAdapterPositionForChildView(View view) {
            if (view.getParent() == mView) {
                return mView.getChildAdapterPosition(view);
            } else {
                return RecyclerView.NO_POSITION;
            }
        }

        @Override
        @Override
        public int getAdapterPositionAt(int index) {
        public int getAdapterPositionAt(int index) {
            View child = mView.getChildAt(index);
            return getAdapterPositionForChildView(mView.getChildAt(index));
            return mView.getChildViewHolder(child).getAdapterPosition();
        }
        }


        @Override
        @Override
@@ -1032,6 +1045,28 @@ public final class MultiSelectManager {
        public void hideBand() {
        public void hideBand() {
            mView.getOverlay().remove(mBand);
            mView.getOverlay().remove(mBand);
        }
        }

        @Override
        public void focusItem(final int pos) {
            // If the item is already in view, focus it; otherwise, scroll to it and focus it.
            RecyclerView.ViewHolder vh = mView.findViewHolderForAdapterPosition(pos);
            if (vh != null) {
                vh.itemView.requestFocus();
            } else {
                // Don't smooth scroll; that taxes the system unnecessarily and makes the scroll
                // handling logic below more complicated.  See b/24865658.
                mView.scrollToPosition(pos);
                // Set a one-time listener to request focus when the scroll has completed.
                mView.addOnScrollListener(
                    new RecyclerView.OnScrollListener() {
                        @Override
                        public void onScrolled(RecyclerView view, int dx, int dy) {
                            view.findViewHolderForAdapterPosition(pos).itemView.requestFocus();
                            view.removeOnScrollListener(this);
                        }
                    });
            }
        }
    }
    }


    public interface Callback {
    public interface Callback {
@@ -1175,7 +1210,6 @@ public final class MultiSelectManager {
        private static final int NOT_SET = -1;
        private static final int NOT_SET = -1;


        private final ItemFinder mItemFinder;
        private final ItemFinder mItemFinder;
        private final BandEnvironment mEnvironment;
        private final Runnable mModelBuilder;
        private final Runnable mModelBuilder;


        @Nullable private Rect mBounds;
        @Nullable private Rect mBounds;
@@ -1188,15 +1222,14 @@ public final class MultiSelectManager {
        private long mScrollStartTime = NOT_SET;
        private long mScrollStartTime = NOT_SET;
        private final Runnable mViewScroller = new ViewScroller();
        private final Runnable mViewScroller = new ViewScroller();


        public BandController(ItemFinder finder, final BandEnvironment environment) {
        public BandController(ItemFinder finder) {
            mItemFinder = finder;
            mItemFinder = finder;
            mEnvironment = environment;
            mEnvironment.addOnScrollListener(this);
            mEnvironment.addOnScrollListener(this);


            mModelBuilder = new Runnable() {
            mModelBuilder = new Runnable() {
                @Override
                @Override
                public void run() {
                public void run() {
                    mModel = new GridModel(environment);
                    mModel = new GridModel(mEnvironment);
                    mModel.addOnSelectionChangedListener(BandController.this);
                    mModel.addOnSelectionChangedListener(BandController.this);
                }
                }
            };
            };
@@ -1459,7 +1492,7 @@ public final class MultiSelectManager {
        private static final int LOWER_LEFT = LOWER | LEFT;
        private static final int LOWER_LEFT = LOWER | LEFT;
        private static final int LOWER_RIGHT = LOWER | RIGHT;
        private static final int LOWER_RIGHT = LOWER | RIGHT;


        private final BandEnvironment mHelper;
        private final SelectionEnvironment mHelper;
        private final List<OnSelectionChangedListener> mOnSelectionChangedListeners =
        private final List<OnSelectionChangedListener> mOnSelectionChangedListeners =
                new ArrayList<>();
                new ArrayList<>();


@@ -1497,7 +1530,7 @@ public final class MultiSelectManager {
        // should expand from when Shift+click is used.
        // should expand from when Shift+click is used.
        private int mPositionNearestOrigin = NOT_SET;
        private int mPositionNearestOrigin = NOT_SET;


        GridModel(BandEnvironment helper) {
        GridModel(SelectionEnvironment helper) {
            mHelper = helper;
            mHelper = helper;
            mHelper.addOnScrollListener(this);
            mHelper.addOnScrollListener(this);
        }
        }
@@ -2041,4 +2074,72 @@ public final class MultiSelectManager {
            return true;
            return true;
        }
        }
    }
    }

    // TODO: Might have to move this to a more global level.  e.g. What should happen if the
    // user taps a file and then presses shift-down?  Currently the RecyclerView never even sees
    // the key event.  Perhaps install a global key handler to catch those events while in
    // selection mode?
    @Override
    public boolean onKey(View view, int keyCode, KeyEvent event) {
        // Listen for key-down events.  This allows the handler to respond appropriately when
        // the user holds down the arrow keys for navigation.
        if (event.getAction() != KeyEvent.ACTION_DOWN) {
            return false;
        }

        int target = RecyclerView.NO_POSITION;
        if (keyCode == KeyEvent.KEYCODE_MOVE_HOME) {
            target = 0;
        } else if (keyCode == KeyEvent.KEYCODE_MOVE_END) {
            target = mAdapter.getItemCount() - 1;
        } else {
            // Find a navigation target based on the arrow key that the user pressed.  Ignore
            // navigation targets that aren't items in the recycler view.
            int searchDir = -1;
            switch (keyCode) {
                case KeyEvent.KEYCODE_DPAD_UP:
                    searchDir = View.FOCUS_UP;
                    break;
                case KeyEvent.KEYCODE_DPAD_DOWN:
                    searchDir = View.FOCUS_DOWN;
                    break;
                case KeyEvent.KEYCODE_DPAD_LEFT:
                    searchDir = View.FOCUS_LEFT;
                    break;
                case KeyEvent.KEYCODE_DPAD_RIGHT:
                    searchDir = View.FOCUS_RIGHT;
                    break;
            }
            if (searchDir != -1) {
                View targetView = view.focusSearch(searchDir);
                target = mEnvironment.getAdapterPositionForChildView(targetView);
            }
        }

        if (target == RecyclerView.NO_POSITION) {
            // If there is no valid navigation target, don't handle the keypress.
            return false;
        }

        // Focus the new file.
        mEnvironment.focusItem(target);

        if (event.isShiftPressed()) {
            if (mSelection.isEmpty()) {
                // If there is no selection, start a selection when the user presses shift-arrow.
                toggleSelection(mEnvironment.getAdapterPositionForChildView(view));
            } else {
                // Deal with b/24802917 (selected items can't be focused) by adjusting the
                // selection sorted the focused item isn't in the selection.
                target -= Integer.signum(target - mRanger.mBegin);
                mRanger.snapSelection(target);
            }
        } else if (!event.isShiftPressed() && !mSelection.isEmpty()) {
            // If there is a selection, clear it if the user presses arrow with no shift.
            clearSelection();
        }

        return true;
    }

}
}
+1 −1
Original line number Original line Diff line number Diff line
@@ -64,7 +64,7 @@ public class DirectoryFragmentModelTest extends AndroidTestCase {
        r.cursor = cursor;
        r.cursor = cursor;


        // Instantiate the model with a dummy view adapter and listener that (for now) do nothing.
        // Instantiate the model with a dummy view adapter and listener that (for now) do nothing.
        model = new Model(mContext, null, new DummyAdapter());
        model = new Model(mContext, new DummyAdapter());
        model.addUpdateListener(new DummyListener());
        model.addUpdateListener(new DummyListener());
        model.update(r);
        model.update(r);
    }
    }
+19 −8
Original line number Original line Diff line number Diff line
@@ -24,6 +24,7 @@ import android.graphics.Rect;
import android.support.v7.widget.RecyclerView.OnScrollListener;
import android.support.v7.widget.RecyclerView.OnScrollListener;
import android.test.AndroidTestCase;
import android.test.AndroidTestCase;
import android.util.SparseBooleanArray;
import android.util.SparseBooleanArray;
import android.view.View;


import com.android.documentsui.MultiSelectManager.GridModel;
import com.android.documentsui.MultiSelectManager.GridModel;


@@ -34,14 +35,14 @@ public class MultiSelectManager_GridModelTest extends AndroidTestCase {
    private static final int VIEWPORT_HEIGHT = 500;
    private static final int VIEWPORT_HEIGHT = 500;


    private static GridModel model;
    private static GridModel model;
    private static TestHelper helper;
    private static TestEnvironment env;
    private static SparseBooleanArray lastSelection;
    private static SparseBooleanArray lastSelection;
    private static int viewWidth;
    private static int viewWidth;


    private static void setUp(int numChildren, int numColumns) {
    private static void setUp(int numChildren, int numColumns) {
        helper = new TestHelper(numChildren, numColumns);
        env = new TestEnvironment(numChildren, numColumns);
        viewWidth = VIEW_PADDING_PX + numColumns * (VIEW_PADDING_PX + CHILD_VIEW_EDGE_PX);
        viewWidth = VIEW_PADDING_PX + numColumns * (VIEW_PADDING_PX + CHILD_VIEW_EDGE_PX);
        model = new GridModel(helper);
        model = new GridModel(env);
        model.addOnSelectionChangedListener(
        model.addOnSelectionChangedListener(
                new GridModel.OnSelectionChangedListener() {
                new GridModel.OnSelectionChangedListener() {
                    @Override
                    @Override
@@ -54,7 +55,7 @@ public class MultiSelectManager_GridModelTest extends AndroidTestCase {
    @Override
    @Override
    public void tearDown() {
    public void tearDown() {
        model = null;
        model = null;
        helper = null;
        env = null;
        lastSelection = null;
        lastSelection = null;
    }
    }


@@ -176,12 +177,12 @@ public class MultiSelectManager_GridModelTest extends AndroidTestCase {
    }
    }


    private static void scroll(int dy) {
    private static void scroll(int dy) {
        assertTrue(helper.verticalOffset + VIEWPORT_HEIGHT + dy <= helper.getTotalHeight());
        assertTrue(env.verticalOffset + VIEWPORT_HEIGHT + dy <= env.getTotalHeight());
        helper.verticalOffset += dy;
        env.verticalOffset += dy;
        model.onScrolled(null, 0, dy);
        model.onScrolled(null, 0, dy);
    }
    }


    private static final class TestHelper implements MultiSelectManager.BandEnvironment {
    private static final class TestEnvironment implements MultiSelectManager.SelectionEnvironment {


        public int horizontalOffset = 0;
        public int horizontalOffset = 0;
        public int verticalOffset = 0;
        public int verticalOffset = 0;
@@ -189,7 +190,7 @@ public class MultiSelectManager_GridModelTest extends AndroidTestCase {
        private final int mNumRows;
        private final int mNumRows;
        private final int mNumChildren;
        private final int mNumChildren;


        public TestHelper(int numChildren, int numColumns) {
        public TestEnvironment(int numChildren, int numColumns) {
            mNumChildren = numChildren;
            mNumChildren = numChildren;
            mNumColumns = numColumns;
            mNumColumns = numColumns;
            mNumRows = (int) Math.ceil((double) numChildren / mNumColumns);
            mNumRows = (int) Math.ceil((double) numChildren / mNumColumns);
@@ -307,5 +308,15 @@ public class MultiSelectManager_GridModelTest extends AndroidTestCase {
        public void removeCallback(Runnable r) {
        public void removeCallback(Runnable r) {
            throw new UnsupportedOperationException();
            throw new UnsupportedOperationException();
        }
        }

        @Override
        public int getAdapterPositionForChildView(View view) {
            throw new UnsupportedOperationException();
        }

        @Override
        public void focusItem(int i) {
            throw new UnsupportedOperationException();
        }
    }
    }
}
}