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

Commit f45629fe authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Addition UserInputHandler test coverage." into nyc-andromeda-dev

parents 8c34c3c5 2d1e4a3b
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;

@@ -280,7 +280,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