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

Commit 2d1e4a3b authored by Steve McKay's avatar Steve McKay
Browse files

Addition UserInputHandler test coverage.

Pull event handling logic out of MultiSelectManager into UserInputHandler.
Pull out a couple common test support classes.

Change-Id: I8958fec9cda05f52192a07a682c318e049871a8d
parent 9666ce69
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -142,7 +142,7 @@ public class DirectoryFragment extends Fragment
    private Model mModel;
    private MultiSelectManager mSelectionMgr;
    private Model.UpdateListener mModelUpdateListener = new ModelUpdateListener();
    private UserInputHandler mInputHandler;
    private UserInputHandler<InputEvent> mInputHandler;
    private SelectionModeListener mSelectionModeListener;
    private FocusManager mFocusManager;

@@ -309,7 +309,7 @@ public class DirectoryFragment extends Fragment
        // Make sure this is done after the RecyclerView is set up.
        mFocusManager = new FocusManager(context, mRecView, mModel);

        mInputHandler = new UserInputHandler(
        mInputHandler = new UserInputHandler<>(
                mSelectionMgr,
                mFocusManager,
                new Function<MotionEvent, InputEvent>() {
+12 −68
Original line number Diff line number Diff line
@@ -21,13 +21,10 @@ import static com.android.documentsui.Shared.DEBUG;
import android.annotation.IntDef;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.v7.widget.RecyclerView;
import android.util.Log;

import com.android.documentsui.Events.InputEvent;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
@@ -39,6 +36,8 @@ import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.Nullable;

/**
 * MultiSelectManager provides support traditional multi-item selection support to RecyclerView.
 * Additionally it can be configured to restrict selection to a single element, @see
@@ -62,7 +61,7 @@ public final class MultiSelectManager {
    private final DocumentsAdapter mAdapter;
    private final List<MultiSelectManager.Callback> mCallbacks = new ArrayList<>(1);

    private Range mRanger;
    private @Nullable Range mRanger;
    private boolean mSingleSelect;

    public MultiSelectManager(DocumentsAdapter adapter, @SelectionMode int mode) {
@@ -223,70 +222,13 @@ public final class MultiSelectManager {
        }
    }

    @VisibleForTesting
    void onLongPress(InputEvent input) {
        if (DEBUG) Log.d(TAG, "Handling long press event.");

        if (!input.isOverItem()) {
            if (DEBUG) Log.i(TAG, "Cannot handle tap. No adapter position available.");
        }

        handleAdapterEvent(input);
    }

    boolean onSingleTapUp(InputEvent input) {
        if (DEBUG) Log.d(TAG, "Processing tap event.");
        if (!hasSelection()) {
            // No selection active - do nothing.
            return false;
        }

        if (!input.isOverItem()) {
            if (DEBUG) Log.d(TAG, "Activity has no position. Canceling selection.");
            clearSelection();
            return false;
        }

        handleAdapterEvent(input);
        return true;
    }

    /**
     * Handles a change caused by a click on the item with the given position. If the Shift key is
     * held down, this performs a range select; otherwise, it simply toggles the item's selection
     * state.
     */
    private void handleAdapterEvent(InputEvent input) {
        if (mRanger != null && input.isShiftKeyDown()) {
            mRanger.snapSelection(input.getItemPosition());
    void snapSelection(int position) {
        mRanger.snapSelection(position);

        // We're being lazy here notifying even when something might not have changed.
        // To make this more correct, we'd need to update the Ranger class to return
        // information about what has changed.
        notifySelectionChanged();
        } else {
            int position = input.getItemPosition();
            toggleSelection(position);
            setSelectionRangeBegin(position);
        }
    }

    /**
     * A convenience method for toggling selection by adapter position.
     *
     * @param position Adapter position to toggle.
     */
    private void toggleSelection(int position) {
        // Position may be special "no position" during certain
        // transitional phases. If so, skip handling of the event.
        if (position == RecyclerView.NO_POSITION) {
            if (DEBUG) Log.d(TAG, "Ignoring toggle for element with no position.");
            return;
        }
        String id = mAdapter.getModelId(position);
        if (id != null) {
            toggleSelection(id);
        }
    }

    /**
@@ -329,7 +271,9 @@ public final class MultiSelectManager {
     * @param pos The new end position for the selection range.
     */
    void snapRangeSelection(int pos) {
        assert(mRanger != null);
        if (!isRangeSelectionActive()) {
            throw new IllegalStateException("Range start point not set.");
        }

        mRanger.snapSelection(pos);
        notifySelectionChanged();
+209 −123
Original line number Diff line number Diff line
@@ -16,6 +16,10 @@

package com.android.documentsui.dirlist;

import static com.android.documentsui.Shared.DEBUG;

import android.support.annotation.VisibleForTesting;
import android.util.Log;
import android.view.GestureDetector;
import android.view.KeyEvent;
import android.view.MotionEvent;
@@ -30,25 +34,29 @@ import java.util.function.Predicate;
/**
 * Grand unified-ish gesture/event listener for items in the directory list.
 */
final class UserInputHandler extends GestureDetector.SimpleOnGestureListener
public final class UserInputHandler<T extends InputEvent>
        extends GestureDetector.SimpleOnGestureListener
        implements KeyboardEventListener {

    private static final String TAG = "UserInputHandler";

    private final MultiSelectManager mSelectionMgr;
    private final FocusHandler mFocusHandler;
    private final Function<MotionEvent, InputEvent> mEventConverter;
    private final Function<InputEvent, DocumentDetails> mDocFinder;
    private final Function<MotionEvent, T> mEventConverter;
    private final Function<T, DocumentDetails> mDocFinder;
    private final Predicate<DocumentDetails> mSelectable;
    private final EventHandler mRightClickHandler;
    private final DocumentHandler mActivateHandler;
    private final DocumentHandler mDeleteHandler;
    private final TouchInputDelegate mTouchDelegate;
    private final MouseInputDelegate mMouseDelegate;
    private final KeyInputHandler mKeyListener;

    public UserInputHandler(
            MultiSelectManager selectionMgr,
            FocusHandler focusHandler,
            Function<MotionEvent, InputEvent> eventConverter,
            Function<InputEvent, DocumentDetails> docFinder,
            Function<MotionEvent, T> eventConverter,
            Function<T, DocumentDetails> docFinder,
            Predicate<DocumentDetails> selectable,
            EventHandler rightClickHandler,
            DocumentHandler activateHandler,
@@ -65,55 +73,116 @@ final class UserInputHandler extends GestureDetector.SimpleOnGestureListener

        mTouchDelegate = new TouchInputDelegate();
        mMouseDelegate = new MouseInputDelegate();
        mKeyListener = new KeyInputHandler();
    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        try (InputEvent event = mEventConverter.apply(e)) {
        try (T event = mEventConverter.apply(e)) {
            return onSingleTapUp(event);
        }
    }

    @VisibleForTesting
    boolean onSingleTapUp(T event) {
        return event.isMouseEvent()
                ? mMouseDelegate.onSingleTapUp(event)
                : mTouchDelegate.onSingleTapUp(event);
    }
    }

    @Override
    public boolean onSingleTapConfirmed(MotionEvent e) {
        try (InputEvent event = mEventConverter.apply(e)) {
        try (T event = mEventConverter.apply(e)) {
            return onSingleTapConfirmed(event);
        }
    }

    @VisibleForTesting
    boolean onSingleTapConfirmed(T event) {
        return event.isMouseEvent()
                ? mMouseDelegate.onSingleTapConfirmed(event)
                : mTouchDelegate.onSingleTapConfirmed(event);
    }
    }

    @Override
    public boolean onDoubleTap(MotionEvent e) {
        try (InputEvent event = mEventConverter.apply(e)) {
        try (T event = mEventConverter.apply(e)) {
            return onDoubleTap(event);
        }
    }

    @VisibleForTesting
    boolean onDoubleTap(T event) {
        return event.isMouseEvent()
                ? mMouseDelegate.onDoubleTap(event)
                : mTouchDelegate.onDoubleTap(event);
    }
    }

    @Override
    public void onLongPress(MotionEvent e) {
        try (InputEvent event = mEventConverter.apply(e)) {
        try (T event = mEventConverter.apply(e)) {
            onLongPress(event);
        }
    }

    @VisibleForTesting
    void onLongPress(T event) {
        if (event.isMouseEvent()) {
            mMouseDelegate.onLongPress(event);
        }
        mTouchDelegate.onLongPress(event);
    }

    public boolean onSingleRightClickUp(MotionEvent e) {
        try (T event = mEventConverter.apply(e)) {
            return mMouseDelegate.onSingleRightClickUp(event);
        }
    }

    @Override
    public boolean onKey(DocumentHolder doc, int keyCode, KeyEvent event) {
        return mKeyListener.onKey(doc, keyCode, event);
    }

    // TODO: Isolate this hack...see if we can't get this solved at the platform level.
    public void setLastButtonState(int state) {
        mMouseDelegate.setLastButtonState(state);
    }

    private boolean activateDocument(DocumentDetails doc) {
        return mActivateHandler.accept(doc);
    }

    private boolean onSelect(DocumentDetails doc) {
    private boolean selectDocument(DocumentDetails doc) {
        assert(doc != null);
        mSelectionMgr.toggleSelection(doc.getModelId());
        mSelectionMgr.setSelectionRangeBegin(doc.getAdapterPosition());
        return true;
    }

    boolean isRangeExtension(T event) {
        return event.isShiftKeyDown() && mSelectionMgr.isRangeSelectionActive();
    }

    private void extendSelectionRange(T event) {
        mSelectionMgr.snapSelection(event.getItemPosition());
    }

    private final class TouchInputDelegate {

        public boolean onSingleTapUp(InputEvent event) {
            if (mSelectionMgr.onSingleTapUp(event)) {
        boolean onSingleTapUp(T event) {
            if (!event.isOverItem()) {
                if (DEBUG) Log.d(TAG, "Tap on non-item. Clearing selection.");
                mSelectionMgr.clearSelection();
                return false;
            }

            if (mSelectionMgr.hasSelection()) {
                if (isRangeExtension(event)) {
                    mSelectionMgr.snapSelection(event.getItemPosition());
                } else {
                    selectDocument(mDocFinder.apply(event));
                }
                return true;
            }

@@ -123,23 +192,31 @@ final class UserInputHandler extends GestureDetector.SimpleOnGestureListener
                // Touch events select if they occur in the selection hotspot,
                // otherwise they activate.
                return doc.isInSelectionHotspot(event)
                        ? onSelect(doc)
                        : mActivateHandler.accept(doc);
                        ? selectDocument(doc)
                        : activateDocument(doc);
            }

            return false;
        }

        public boolean onSingleTapConfirmed(InputEvent event) {
        boolean onSingleTapConfirmed(T event) {
            return false;
        }

        public boolean onDoubleTap(InputEvent event) {
        boolean onDoubleTap(T event) {
            return false;
        }

        public void onLongPress(InputEvent event) {
            mSelectionMgr.onLongPress(event);
        final void onLongPress(T event) {
            if (!event.isOverItem()) {
                return;
            }

            if (isRangeExtension(event)) {
                extendSelectionRange(event);
            } else {
                selectDocument(mDocFinder.apply(event));
            }
        }
    }

@@ -155,18 +232,28 @@ final class UserInputHandler extends GestureDetector.SimpleOnGestureListener
        private int mLastButtonState = -1;

        // true when the previous event has consumed a right click motion event
        private boolean ateRightClick;
        private boolean mAteRightClick;

        // The event has been handled in onSingleTapUp
        private boolean handledTapUp;
        private boolean mHandledTapUp;

        public boolean onSingleTapUp(InputEvent event) {
        boolean onSingleTapUp(T event) {
            if (eatRightClick()) {
                return onSingleRightClickUp(event);
            }

            if (mSelectionMgr.onSingleTapUp(event)) {
                handledTapUp = true;
            if (!event.isOverItem()) {
                mSelectionMgr.clearSelection();
                return false;
            }

            if (mSelectionMgr.hasSelection()) {
                if (isRangeExtension(event)) {
                    extendSelectionRange(event);
                } else {
                    selectDocument(mDocFinder.apply(event));
                }
                mHandledTapUp = true;
                return true;
            }

@@ -181,17 +268,17 @@ final class UserInputHandler extends GestureDetector.SimpleOnGestureListener
                return false;
            }

            handledTapUp = true;
            return onSelect(doc);
            mHandledTapUp = true;
            return selectDocument(doc);
        }

        public boolean onSingleTapConfirmed(InputEvent event) {
            if (ateRightClick) {
                ateRightClick = false;
        boolean onSingleTapConfirmed(T event) {
            if (mAteRightClick) {
                mAteRightClick = false;
                return false;
            }
            if (handledTapUp) {
                handledTapUp = false;
            if (mHandledTapUp) {
                mHandledTapUp = false;
                return false;
            }

@@ -204,25 +291,33 @@ final class UserInputHandler extends GestureDetector.SimpleOnGestureListener
                return false;
            }

            return onSelect(doc);
            return selectDocument(doc);
        }

        public boolean onDoubleTap(InputEvent event) {
            handledTapUp = false;
        boolean onDoubleTap(T event) {
            mHandledTapUp = false;
            DocumentDetails doc = mDocFinder.apply(event);
            if (doc != null) {
                return mSelectionMgr.hasSelection()
                        ? onSelect(doc)
                        : mActivateHandler.accept(doc);
                        ? selectDocument(doc)
                        : activateDocument(doc);
            }
            return false;
        }

        public void onLongPress(InputEvent event) {
            mSelectionMgr.onLongPress(event);
        final void onLongPress(T event) {
            if (!event.isOverItem()) {
                return;
            }

            if (isRangeExtension(event)) {
                extendSelectionRange(event);
            } else {
                selectDocument(mDocFinder.apply(event));
            }
        }

        private boolean onSingleRightClickUp(InputEvent event) {
        private boolean onSingleRightClickUp(T event) {
            return mRightClickHandler.apply(event);
        }

@@ -234,28 +329,18 @@ final class UserInputHandler extends GestureDetector.SimpleOnGestureListener
        private boolean eatRightClick() {
            if (mLastButtonState == MotionEvent.BUTTON_SECONDARY) {
                mLastButtonState = -1;
                ateRightClick = true;
                mAteRightClick = true;
                return true;
            }
            return false;
        }
    }

    public boolean onSingleRightClickUp(MotionEvent e) {
        try (InputEvent event = mEventConverter.apply(e)) {
            return mMouseDelegate.onSingleRightClickUp(event);
        }
    }

    // TODO: Isolate this hack...see if we can't get this solved at the platform level.
    public void setLastButtonState(int state) {
        mMouseDelegate.setLastButtonState(state);
    }

    private final class KeyInputHandler {
        // TODO: Refactor FocusManager to depend only on DocumentDetails so we can eliminate
        // difficult to test dependency on DocumentHolder.
    @Override
    public boolean onKey(DocumentHolder doc, int keyCode, KeyEvent event) {

        boolean onKey(DocumentHolder doc, int keyCode, KeyEvent event) {
            // Only handle key-down events. This is simpler, consistent with most other UIs, and
            // enables the handling of repeated key events from holding down a key.
            if (event.getAction() != KeyEvent.ACTION_DOWN) {
@@ -287,12 +372,12 @@ final class UserInputHandler extends GestureDetector.SimpleOnGestureListener
            switch (keyCode) {
                case KeyEvent.KEYCODE_ENTER:
                    if (event.isShiftPressed()) {
                    onSelect(doc);
                        selectDocument(doc);
                    }
                    // For non-shifted enter keypresses, fall through.
                case KeyEvent.KEYCODE_DPAD_CENTER:
                case KeyEvent.KEYCODE_BUTTON_A:
                return mActivateHandler.accept(doc);
                    return activateDocument(doc);
                case KeyEvent.KEYCODE_FORWARD_DEL:
                    // This has to be handled here instead of in a keyboard shortcut, because
                    // keyboard shortcuts all have to be modified with the 'Ctrl' key.
@@ -315,15 +400,6 @@ final class UserInputHandler extends GestureDetector.SimpleOnGestureListener

            return mSelectable.test(doc);
        }

    @FunctionalInterface
    interface EventHandler {
        boolean apply(InputEvent input);
    }

    @FunctionalInterface
    interface DocumentHandler {
        boolean accept(DocumentDetails doc);
    }

    /**
@@ -334,4 +410,14 @@ final class UserInputHandler extends GestureDetector.SimpleOnGestureListener
        int getAdapterPosition();
        boolean isInSelectionHotspot(InputEvent event);
    }

    @FunctionalInterface
    interface EventHandler {
        boolean apply(InputEvent event);
    }

    @FunctionalInterface
    interface DocumentHandler {
        boolean accept(DocumentDetails doc);
    }
}
+34 −232

File changed.

Preview size limit exceeded, changes collapsed.

+66 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.documentsui.dirlist;

import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;

import com.android.documentsui.testing.dirlist.SelectionProbe;
import com.android.documentsui.testing.dirlist.TestSelectionListener;

import java.util.List;

@SmallTest
public class MultiSelectManager_SingleSelectTest extends AndroidTestCase {

    private static final List<String> ITEMS = TestData.create(100);

    private MultiSelectManager mManager;
    private TestSelectionListener mCallback;
    private TestDocumentsAdapter mAdapter;
    private SelectionProbe mSelection;

    @Override
    public void setUp() throws Exception {
        mCallback = new TestSelectionListener();
        mAdapter = new TestDocumentsAdapter(ITEMS);
        mManager = new MultiSelectManager(mAdapter, MultiSelectManager.MODE_SINGLE);
        mManager.addCallback(mCallback);

        mSelection = new SelectionProbe(mManager);
    }

    public void testSimpleSelect() {
        mManager.toggleSelection(ITEMS.get(3));
        mManager.toggleSelection(ITEMS.get(4));
        mCallback.assertSelectionChanged();
        mSelection.assertSelection(4);
    }

    public void testRangeSelectionNotEstablished() {
        mManager.toggleSelection(ITEMS.get(3));
        mCallback.reset();

        try {
            mManager.snapRangeSelection(10);
            fail("Should have thrown.");
        } catch (Exception expected) {}

        mCallback.assertSelectionUnchanged();
        mSelection.assertSelection(3);
    }
}
Loading