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

Commit 1fdd34b1 authored by Steve McKay's avatar Steve McKay
Browse files

Segregate Mouse and Touch input handling.

- The change also isolates these respective classes from DocumentsUI
    by the introduction of ItemDetailsLookup && ItemDetails.
- Replace DocumentDetails w/ ItemDetails.
- Introduce a pattern in the code of using input-type specific callbacks
    (one each for mouse, touch, and keyboard)
    in order to allow "openDocument" behavior to be specialized.
    DocumentsUI has some nuanced behaviors around opening that weren't
    appropriate for inclusion in support lib.
- Move focus related access to callbacks...focus is outside of the scope
    of this change (and likely this entire effort).
- Cleanly separate gesture event delegation.
- Address outstanding commments from: ag/2924928

Change-Id: Ibc26bb66ac805cd1f2345e7f01cedcf865c8467d
Bug: 64847011
Test: Passing (and substantially massaged to support the refactoring).
parent bc547b16
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -51,7 +51,6 @@ import com.android.documentsui.base.Shared;
import com.android.documentsui.base.State;
import com.android.documentsui.dirlist.AnimationView;
import com.android.documentsui.dirlist.AnimationView.AnimationType;
import com.android.documentsui.dirlist.DocumentDetails;
import com.android.documentsui.dirlist.FocusHandler;
import com.android.documentsui.files.LauncherActivity;
import com.android.documentsui.queries.SearchViewManager;
@@ -61,6 +60,7 @@ import com.android.documentsui.roots.ProvidersAccess;
import com.android.documentsui.selection.MutableSelection;
import com.android.documentsui.selection.SelectionHelper;
import com.android.documentsui.selection.addons.ContentLock;
import com.android.documentsui.selection.addons.ItemDetailsLookup.ItemDetails;
import com.android.documentsui.sidebar.EjectRootTask;
import com.android.documentsui.ui.Snackbars;

@@ -227,7 +227,7 @@ public abstract class AbstractActionHandler<T extends Activity & CommonAddons>
    }

    @Override
    public boolean openDocument(DocumentDetails doc, @ViewType int type, @ViewType int fallback) {
    public boolean openItem(ItemDetails doc, @ViewType int type, @ViewType int fallback) {
        throw new UnsupportedOperationException("Can't open document.");
    }

+2 −2
Original line number Diff line number Diff line
@@ -28,8 +28,8 @@ import com.android.documentsui.base.BooleanConsumer;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.DocumentStack;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.dirlist.DocumentDetails;
import com.android.documentsui.selection.addons.ContentLock;
import com.android.documentsui.selection.addons.ItemDetailsLookup.ItemDetails;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -110,7 +110,7 @@ public interface ActionHandler {
     * If container, then opens the container, otherwise views using the specified type of view.
     * If the primary view type is unavailable, then fallback to the alternative type of view.
     */
    boolean openDocument(DocumentDetails doc, @ViewType int type, @ViewType int fallback);
    boolean openItem(ItemDetails doc, @ViewType int type, @ViewType int fallback);

    /**
     * This is called when user hovers over a doc for enough time during a drag n' drop, to open a
+53 −115
Original line number Diff line number Diff line
@@ -16,7 +16,6 @@

package com.android.documentsui.dirlist;

import static com.android.documentsui.base.DocumentInfo.getCursorInt;
import static com.android.documentsui.base.DocumentInfo.getCursorString;
import static com.android.documentsui.base.Shared.DEBUG;
import static com.android.documentsui.base.Shared.VERBOSE;
@@ -41,6 +40,7 @@ import android.os.Handler;
import android.os.Parcelable;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.support.annotation.Nullable;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.GridLayoutManager.SpanSizeLookup;
@@ -50,7 +50,6 @@ import android.support.v7.widget.RecyclerView.ViewHolder;
import android.util.Log;
import android.util.SparseArray;
import android.view.ContextMenu;
import android.view.HapticFeedbackConstants;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -75,8 +74,6 @@ import com.android.documentsui.ThumbnailCache;
import com.android.documentsui.base.DocumentFilters;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.DocumentStack;
import com.android.documentsui.base.EventDetailsLookup;
import com.android.documentsui.base.EventHandler;
import com.android.documentsui.base.EventListener;
import com.android.documentsui.base.Features;
import com.android.documentsui.base.RootInfo;
@@ -95,7 +92,13 @@ import com.android.documentsui.selection.addons.BandPredicate;
import com.android.documentsui.selection.addons.BandSelectionHelper;
import com.android.documentsui.selection.addons.ContentLock;
import com.android.documentsui.selection.addons.DefaultBandHost;
import com.android.documentsui.selection.addons.DefaultBandPredicate;
import com.android.documentsui.selection.addons.GestureSelectionHelper;
import com.android.documentsui.selection.addons.InputEventDispatcher;
import com.android.documentsui.selection.addons.ItemDetailsLookup;
import com.android.documentsui.selection.addons.KeyInputHandler;
import com.android.documentsui.selection.addons.MouseInputHandler;
import com.android.documentsui.selection.addons.TouchInputHandler;
import com.android.documentsui.services.FileOperation;
import com.android.documentsui.services.FileOperationService;
import com.android.documentsui.services.FileOperationService.OpType;
@@ -108,8 +111,6 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;

import javax.annotation.Nullable;

/**
 * Display the documents inside a single directory.
 */
@@ -125,7 +126,7 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On
    public @interface RequestCode {}
    public static final int REQUEST_COPY_DESTINATION = 1;

    private static final String TAG = "DirectoryFragment";
    static final String TAG = "DirectoryFragment";
    private static final int LOADER_ID = 42;

    private static final int CACHE_EVICT_LIMIT = 100;
@@ -158,15 +159,15 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On
    @ContentScoped
    private ActionModeController mActionModeController;

    private EventDetailsLookup mDetailsLookup;
    private ItemDetailsLookup mDetailsLookup;
    private SelectionMetadata mSelectionMetadata;
    private UserInputHandler mInputHandler;
    private InputEventDispatcher mInputHandler;
    private KeyInputHandler mKeyListener;
    private @Nullable BandSelectionHelper mBandSelector;
    private @Nullable DragHoverListener mDragHoverListener;
    private IconHelper mIconHelper;
    private SwipeRefreshLayout mRefreshLayout;
    private RecyclerView mRecView;

    private DocumentsAdapter mAdapter;
    private DocumentClipper mClipper;
    private GridLayoutManager mLayout;
@@ -320,31 +321,8 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On
        mModel.addUpdateListener(mAdapter.getModelUpdateListener());
        mModel.addUpdateListener(mModelUpdateListener);

        SelectionPredicate selectionPredicate = new SelectionPredicate() {

            @Override
            public boolean canSetStateForId(String id, boolean nextState) {
                return canSetSelectionState(id, nextState);
            }

            @Override
            public boolean canSetStateAtPosition(int position, boolean nextState) {
                // This method features a nextState arg for symmetry.
                // But, there are no current uses for checking un-selecting state by position.
                // So rather than have some unsuspecting client think canSetState(int, false)
                // will ever do anything. Let's just be grumpy about it.
                assert nextState == true;

                // NOTE: Given that we have logic in some places disallowing selection,
                // it may be a bug that Band and Gesture based selections don't
                // also verify something can be unselected.

                // The band selection model only operates on documents and directories.
                // Exclude other types of adapter items like whitespace and dividers.
                RecyclerView.ViewHolder vh = mRecView.findViewHolderForAdapterPosition(position);
                return ModelBackedDocumentsAdapter.isContentType(vh.getItemViewType());
            }
        };
        SelectionPredicate selectionPredicate =
                new DocsSelectionPredicate(mInjector.config, mState, mModel, mRecView);

        mSelectionMgr = mInjector.getSelectionManager(mAdapter, selectionPredicate);
        mFocusManager = mInjector.getFocusManager(mRecView, mModel);
@@ -355,22 +333,13 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On
                        (View child) -> onAccessibilityClick(child)));
        mSelectionMetadata = new SelectionMetadata(mModel::getItem);
        mSelectionMgr.addObserver(mSelectionMetadata);
        mDetailsLookup = new RuntimeEventDetailsLookup(mRecView);
        mDetailsLookup = new DocsItemDetailsLookup(mRecView);

        GestureSelectionHelper gestureSel =
                GestureSelectionHelper.create(mSelectionMgr, mRecView, mContentLock);

        if (mState.allowMultiple) {
            BandPredicate bandPredicate = new BandPredicate() {
                @Override
                public boolean canInitiate(MotionEvent e) {
                    View view = mRecView.findChildViewUnder(e.getX(), e.getY());
                    if (view instanceof DocumentDetails) {
                        return ((DocumentDetails) view).inDragRegion(e);
                    }
                    return true;
                }
            };
            BandPredicate bandPredicate = new DefaultBandPredicate(mDetailsLookup);

            mBandSelector = new BandSelectionHelper(
                    new DefaultBandHost(mRecView, R.drawable.band_select_overlay, bandPredicate),
@@ -384,7 +353,7 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On
            mBandSelector.addOnBandStartedListener(mBandSelectStartedCallback);
        }

        DragStartListener mDragStartListener = mInjector.config.dragAndDropEnabled()
        DragStartListener dragStartListener = mInjector.config.dragAndDropEnabled()
                ? DragStartListener.create(
                        mIconHelper,
                        mModel,
@@ -397,43 +366,45 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On
                        DocumentsApplication.getDragAndDropManager(mActivity))
                : DragStartListener.DUMMY;

        EventHandler<MotionEvent> gestureHandler = mState.allowMultiple
                ? new EventHandler<MotionEvent>() {
                    @Override
                    public boolean accept(MotionEvent event) {
                        return gestureSel.start();
                    }
                }
                : EventHandler.createStub(false);

        mInputHandler = new UserInputHandler(
        // Construction of the input handlers is non trivial, so to keep logic clear,
        // and code flexible, and DirectoryFragment small, the construction has been
        // moved off into a separate class.
        InputHandlers handlers = new InputHandlers(
                mActions,
                mFocusManager,
                mSelectionMgr,
                selectionPredicate,
                mDetailsLookup,
                this::canSelect,
                this::onContextMenuClick,
                mDragStartListener::onTouchDragEvent,
                gestureHandler,
                () -> mRecView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS));
                mFocusManager,
                mRecView,
                mState);

        TouchInputHandler touchDelegate =
                handlers.createTouchHandler(gestureSel, dragStartListener);

        MouseInputHandler mouseDelegate = handlers.createMouseHandler(this::onContextMenuClick);

        mInputHandler = new InputEventDispatcher(touchDelegate);  // default handler.
        mInputHandler.register(MotionEvent.TOOL_TYPE_MOUSE, mouseDelegate);

        mKeyListener = handlers.createKeyHandler();

        if (Build.IS_DEBUGGABLE) {
            new ScaleHelper(this.getContext(), mInjector.features, this::scaleLayout)
                    .install(mRecView);
                    .attach(mRecView);
        }

        new RefreshHelper(mRefreshLayout::setEnabled)
                .install(mRecView);
                .attach(mRecView);

        ListeningGestureDetector recListener = new ListeningGestureDetector(
                this.getContext(),
                mDetailsLookup,
                mDragStartListener::onMouseDragEvent,
                dragStartListener::onMouseDragEvent,
                gestureSel,
                mInputHandler,
                mBandSelector);

        recListener.listenTo(mRecView);
        recListener.attach(mRecView);

        mActionModeController = mInjector.getActionModeController(
                mSelectionMetadata,
@@ -460,6 +431,7 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On
        mActions.loadDocumentsForCurrentStack();
    }


    @Override
    public void onStart() {
        super.onStart();
@@ -539,24 +511,19 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On

    // TODO: Move to UserInputHander.
    protected boolean onContextMenuClick(MotionEvent e) {
        final View v;
        final float x, y;
        if (mDetailsLookup.overModelItem(e)) {
            // Oooo. Naughty. This is test hostile code, since it makes assumptions
            // about the document details being a holder.
            DocumentHolder doc = (DocumentHolder) mDetailsLookup.getDocumentDetails(e);

            v = doc.itemView;
            x = e.getX() - v.getLeft();
            y = e.getY() - v.getTop();
        } else {
            v = mRecView;
            x = e.getX();
            y = e.getY();
        }

        mInjector.menuManager.showContextMenu(this, v, x, y);
        if (mDetailsLookup.overStableItem(e)) {
            View childView = mRecView.findChildViewUnder(e.getX(), e.getY());
            ViewHolder holder = mRecView.getChildViewHolder(childView);

            View view = holder.itemView;
            float x = e.getX() - view.getLeft();
            float y = e.getY() - view.getTop();
            mInjector.menuManager.showContextMenu(this, view, x, y);
            return true;
        }

        mInjector.menuManager.showContextMenu(this, mRecView, e.getX(), e.getY());
        return true;
    }

@@ -784,8 +751,8 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On
    }

    private boolean onAccessibilityClick(View child) {
        DocumentDetails doc = getDocumentHolder(child);
        mActions.openDocument(doc, ActionHandler.VIEW_TYPE_PREVIEW,
        DocumentHolder holder = getDocumentHolder(child);
        mActions.openItem(holder.getItemDetails(), ActionHandler.VIEW_TYPE_PREVIEW,
                ActionHandler.VIEW_TYPE_REGULAR);
        return true;
    }
@@ -946,10 +913,6 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On
        return mModel;
    }

    private boolean isDocumentEnabled(String mimeType, int flags) {
        return mInjector.config.isDocumentEnabled(mimeType, flags, mState);
    }

    /**
     * Paste selection files from the primary clip into the current window.
     */
@@ -1032,31 +995,6 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On
        return null;
    }

    // TODO: Move to activities when Model becomes activity level object.
    private boolean canSelect(DocumentDetails doc) {
        return canSetSelectionState(doc.getModelId(), true);
    }

    // TODO: Move to activities when Model becomes activity level object.
    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;
            }

            final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
            final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
            return mInjector.config.canSelectType(docMimeType, docFlags, mState);
        } else {
            final DocumentInfo parent = mActivity.getCurrentDirectory();
            // Right now all selected items can be deselected.
            return true;
        }
    }

    public static void showDirectory(
            FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) {
        if (DEBUG) Log.d(TAG, "Showing directory: " + DocumentInfo.debugString(doc));
@@ -1225,7 +1163,7 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On

        @Override
        public void initDocumentHolder(DocumentHolder holder) {
            holder.addKeyEventListener(mInputHandler);
            holder.addKeyEventListener(mKeyListener);
            holder.itemView.setOnFocusChangeListener(mFocusManager);
        }

+23 −12
Original line number Diff line number Diff line
@@ -15,20 +15,22 @@
 */
package com.android.documentsui.dirlist;

import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.view.MotionEvent;
import android.view.View;

import com.android.documentsui.base.EventDetailsLookup;
import com.android.documentsui.selection.addons.ItemDetailsLookup;

/**
 * Access to document details relating to {@link MotionEvent} instances.
 * Access to details of an item associated with a {@link MotionEvent} instance.
 */
final class RuntimeEventDetailsLookup implements EventDetailsLookup {
final class DocsItemDetailsLookup extends ItemDetailsLookup {

    private final RecyclerView mRecView;

    public RuntimeEventDetailsLookup(RecyclerView view) {
    public DocsItemDetailsLookup(RecyclerView view) {
        mRecView = view;
    }

@@ -38,18 +40,18 @@ final class RuntimeEventDetailsLookup implements EventDetailsLookup {
    }

    @Override
    public boolean overModelItem(MotionEvent e) {
        return overItem(e) && getDocumentDetails(e).hasModelId();
    public boolean overStableItem(MotionEvent e) {
        return overItem(e) && getItemDetails(e).hasStableId();
    }

    @Override
    public boolean inItemDragRegion(MotionEvent e) {
        return overItem(e) && getDocumentDetails(e).inDragRegion(e);
        return overItem(e) && getItemDetails(e).inDragRegion(e);
    }

    @Override
    public boolean inItemSelectRegion(MotionEvent e) {
        return overItem(e) && getDocumentDetails(e).inSelectRegion(e);
        return overItem(e) && getItemDetails(e).inSelectionHotspot(e);
    }

    @Override
@@ -61,10 +63,19 @@ final class RuntimeEventDetailsLookup implements EventDetailsLookup {
    }

    @Override
    public DocumentDetails getDocumentDetails(MotionEvent e) {
    public ItemDetails getItemDetails(MotionEvent e) {
        @Nullable DocumentHolder holder = getDocumentHolder(e);
        return holder == null ? null : holder.getItemDetails();
    }

    private @Nullable DocumentHolder getDocumentHolder(MotionEvent e) {
        View childView = mRecView.findChildViewUnder(e.getX(), e.getY());
        return (childView != null)
            ? (DocumentHolder) mRecView.getChildViewHolder(childView)
            : null;
        if (childView != null) {
            ViewHolder holder = mRecView.getChildViewHolder(childView);
            if (holder instanceof DocumentHolder) {
                return (DocumentHolder) holder;
            }
        }
        return null;
    }
}
+94 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.documentsui.dirlist;

import static android.support.v4.util.Preconditions.checkArgument;
import static com.android.documentsui.base.DocumentInfo.getCursorInt;
import static com.android.documentsui.base.DocumentInfo.getCursorString;

import android.database.Cursor;
import android.provider.DocumentsContract.Document;
import android.support.v7.widget.RecyclerView;
import android.util.Log;

import com.android.documentsui.ActivityConfig;
import com.android.documentsui.Model;
import com.android.documentsui.base.State;
import com.android.documentsui.selection.SelectionHelper.SelectionPredicate;

/**
 * Class embodying the logic as to whether an item (specified by id or position)
 * can be selected (or not).
 */
final class DocsSelectionPredicate extends SelectionPredicate {

    private ActivityConfig mConfig;
    private Model mModel;
    private RecyclerView mRecView;
    private State mState;

    DocsSelectionPredicate(
            ActivityConfig config, State state, Model model, RecyclerView recView) {

        checkArgument(config != null);
        checkArgument(state != null);
        checkArgument(model != null);
        checkArgument(recView != null);

        mConfig = config;
        mState = state;
        mModel = model;
        mRecView = recView;

    }

    @Override
    public boolean canSetStateForId(String id, boolean nextState) {
        if (nextState) {
            // Check if an item can be selected
            final Cursor cursor = mModel.getItem(id);
            if (cursor == null) {
                Log.w(DirectoryFragment.TAG, "Couldn't obtain cursor for id: " + id);
                return false;
            }

            final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
            final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
            return mConfig.canSelectType(docMimeType, docFlags, mState);
        }

        // Right now all selected items can be deselected.
        return true;
    }

    @Override
    public boolean canSetStateAtPosition(int position, boolean nextState) {
        // This method features a nextState arg for symmetry.
        // But, there are no current uses for checking un-selecting state by position.
        // So rather than have some unsuspecting client think canSetState(int, false)
        // will ever do anything. Let's just be grumpy about it.
        assert nextState == true;

        // NOTE: Given that we have logic in some places disallowing selection,
        // it may be a bug that Band and Gesture based selections don't
        // also verify something can be unselected.

        // The band selection model only operates on documents and directories.
        // Exclude other types of adapter items like whitespace and dividers.
        RecyclerView.ViewHolder vh = mRecView.findViewHolderForAdapterPosition(position);
        return ModelBackedDocumentsAdapter.isContentType(vh.getItemViewType());
    }
}
 No newline at end of file
Loading