diff --git a/packages/DocumentsUI/src/com/android/documentsui/Events.java b/packages/DocumentsUI/src/com/android/documentsui/Events.java index 116292365dfa5..691f95a67af75 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/Events.java +++ b/packages/DocumentsUI/src/com/android/documentsui/Events.java @@ -138,7 +138,7 @@ public final class Events { int getItemPosition(); } - public static final class MotionInputEvent implements InputEvent { + public static final class MotionInputEvent implements InputEvent, AutoCloseable { private static final String TAG = "MotionInputEvent"; private static final Pools.SimplePool sPool = new Pools.SimplePool<>(1); @@ -199,6 +199,11 @@ public final class Events { assert(released); } + @Override + public void close() { + recycle(); + } + @Override public boolean isMouseEvent() { return Events.isMouseEvent(mEvent); diff --git a/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java b/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java index 7bdfe8e38cee0..019ca86bd3db6 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java +++ b/packages/DocumentsUI/src/com/android/documentsui/RecentsCreateFragment.java @@ -52,13 +52,13 @@ import com.android.documentsui.model.DocumentStack; import com.android.documentsui.model.DurableUtils; import com.android.documentsui.model.RootInfo; -import libcore.io.IoUtils; - import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import libcore.io.IoUtils; + /** * Display directories where recent creates took place. */ @@ -141,17 +141,13 @@ public class RecentsCreateFragment extends Fragment { new RecyclerView.OnItemTouchListener() { @Override public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { - final MotionInputEvent event = MotionInputEvent.obtain(e, mRecView); - try { + try (MotionInputEvent event = MotionInputEvent.obtain(e, mRecView)) { if (event.isOverItem() && event.isActionUp()) { final DocumentStack stack = mAdapter.getItem(event.getItemPosition()); ((BaseActivity) getActivity()).onStackPicked(stack); return true; } - return false; - } finally { - event.recycle(); } } @@ -247,7 +243,7 @@ public class RecentsCreateFragment extends Fragment { final LayoutInflater inflater = LayoutInflater.from(context); return new StackHolder( - (View) inflater.inflate(R.layout.item_doc_list, parent, false)); + inflater.inflate(R.layout.item_doc_list, parent, false)); } @Override diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/BandController.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/BandController.java index 9581b3e7bd971..7320dc0ef98fb 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/BandController.java +++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/BandController.java @@ -85,21 +85,15 @@ public class BandController extends RecyclerView.OnScrollListener { new RecyclerView.OnItemTouchListener() { @Override public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { - final MotionInputEvent event = MotionInputEvent.obtain(e, view); - try { + try (MotionInputEvent event = MotionInputEvent.obtain(e, view)) { return handleEvent(event); - } finally { - event.recycle(); } } @Override public void onTouchEvent(RecyclerView rv, MotionEvent e) { if (Events.isMouseEvent(e)) { - final MotionInputEvent event = MotionInputEvent.obtain(e, view); - try { + try (MotionInputEvent event = MotionInputEvent.obtain(e, view)) { processInputEvent(event); - } finally { - event.recycle(); } } } diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java index 86c6c9944ca01..3a80e208c12ec 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java +++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java @@ -58,13 +58,11 @@ import android.util.SparseArray; import android.view.ActionMode; import android.view.ContextMenu; import android.view.DragEvent; -import android.view.GestureDetector; import android.view.HapticFeedbackConstants; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; -import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; @@ -74,11 +72,9 @@ import android.widget.Toolbar; import com.android.documentsui.BaseActivity; import com.android.documentsui.DirectoryLoader; import com.android.documentsui.DirectoryResult; -import com.android.documentsui.UrisSupplier; import com.android.documentsui.DocumentClipper; import com.android.documentsui.DocumentsActivity; import com.android.documentsui.DocumentsApplication; -import com.android.documentsui.Events; import com.android.documentsui.Events.MotionInputEvent; import com.android.documentsui.ItemDragListener; import com.android.documentsui.MenuManager; @@ -94,6 +90,7 @@ import com.android.documentsui.Shared; import com.android.documentsui.Snackbars; import com.android.documentsui.State; import com.android.documentsui.State.ViewMode; +import com.android.documentsui.UrisSupplier; import com.android.documentsui.dirlist.MultiSelectManager.Selection; import com.android.documentsui.model.DocumentInfo; import com.android.documentsui.model.RootInfo; @@ -295,22 +292,27 @@ public class DirectoryFragment extends Fragment } mRecView.setLayoutManager(mLayout); - mGestureDetector = - new ListeningGestureDetector(this.getContext(), mDragHelper, new GestureListener()); - - mRecView.addOnItemTouchListener(mGestureDetector); - mEmptyView.setOnTouchListener(mGestureDetector); - // 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 // into the selection manager. mSelectionManager = new MultiSelectManager( - mRecView, mAdapter, state.allowMultiple ? MultiSelectManager.MODE_MULTIPLE - : MultiSelectManager.MODE_SINGLE, - null); + : MultiSelectManager.MODE_SINGLE); + + GestureListener gestureListener = new GestureListener( + mSelectionManager, + mRecView, + this::getTarget, + this::onDoubleTap, + this::onRightClick); + + mGestureDetector = + new ListeningGestureDetector(this.getContext(), mDragHelper, gestureListener); + + mRecView.addOnItemTouchListener(mGestureDetector); + mEmptyView.setOnTouchListener(mGestureDetector); if (state.allowMultiple) { mBandController = new BandController(mRecView, mAdapter, mSelectionManager); @@ -426,44 +428,38 @@ public class DirectoryFragment extends Fragment return false; } - protected boolean onRightClick(MotionEvent e) { - // First get target to see if it's a blank window or a file/doc - final MotionInputEvent event = MotionInputEvent.obtain(e, mRecView); - try { - if (event.getItemPosition() != RecyclerView.NO_POSITION) { - final DocumentHolder holder = getTarget(event); - String modelId = getModelId(holder.itemView); - if (!mSelectionManager.getSelection().contains(modelId)) { - mSelectionManager.clearSelection(); - // Set selection on the one single item - List ids = Collections.singletonList(modelId); - mSelectionManager.setItemsSelected(ids, true); - } + protected boolean onRightClick(MotionInputEvent e) { + if (e.getItemPosition() != RecyclerView.NO_POSITION) { + final DocumentHolder holder = getTarget(e); + String modelId = getModelId(holder.itemView); + if (!mSelectionManager.getSelection().contains(modelId)) { + mSelectionManager.clearSelection(); + // Set selection on the one single item + List ids = Collections.singletonList(modelId); + mSelectionManager.setItemsSelected(ids, true); + } - // We are registering for context menu here so long-press doesn't trigger this - // floating context menu, and then quickly unregister right afterwards - registerForContextMenu(holder.itemView); - mRecView.showContextMenuForChild(holder.itemView, - e.getX() - holder.itemView.getLeft(), e.getY() - holder.itemView.getTop()); - unregisterForContextMenu(holder.itemView); - } - // If there was no corresponding item pos, that means user right-clicked on the blank - // pane - // We would want to show different options then, and not select any item - // The blank pane could be the recyclerView or the emptyView, so we need to register - // according to whichever one is visible - else if (mEmptyView.getVisibility() == View.VISIBLE) { - registerForContextMenu(mEmptyView); - mEmptyView.showContextMenu(e.getX(), e.getY()); - unregisterForContextMenu(mEmptyView); - return true; - } else { - registerForContextMenu(mRecView); - mRecView.showContextMenu(e.getX(), e.getY()); - unregisterForContextMenu(mRecView); - } - } finally { - event.recycle(); + // We are registering for context menu here so long-press doesn't trigger this + // floating context menu, and then quickly unregister right afterwards + registerForContextMenu(holder.itemView); + mRecView.showContextMenuForChild(holder.itemView, + e.getX() - holder.itemView.getLeft(), e.getY() - holder.itemView.getTop()); + unregisterForContextMenu(holder.itemView); + } + // If there was no corresponding item pos, that means user right-clicked on the blank + // pane + // We would want to show different options then, and not select any item + // The blank pane could be the recyclerView or the emptyView, so we need to register + // according to whichever one is visible + else if (mEmptyView.getVisibility() == View.VISIBLE) { + registerForContextMenu(mEmptyView); + mEmptyView.showContextMenu(e.getX(), e.getY()); + unregisterForContextMenu(mEmptyView); + return true; + } else { + registerForContextMenu(mRecView); + mRecView.showContextMenu(e.getX(), e.getY()); + unregisterForContextMenu(mRecView); } return true; } @@ -1554,81 +1550,6 @@ public class DirectoryFragment extends Fragment return mTuner.canSelectType(docMimeType, docFlags); } - /** - * The gesture listener for items in the list/grid view. Interprets gestures and sends the - * events to the target DocumentHolder, whence they are routed to the appropriate listener. - */ - class GestureListener extends GestureDetector.SimpleOnGestureListener { - // From the RecyclerView, we get two events sent to - // ListeningGestureDetector#onInterceptTouchEvent on a mouse click; we first get an - // ACTION_DOWN Event for clicking on the mouse, and then an ACTION_UP event from releasing - // the mouse click. ACTION_UP event doesn't have information regarding the button (primary - // vs. secondary), so we have to save that somewhere first from ACTION_DOWN, and then reuse - // it later. The ACTION_DOWN event doesn't get forwarded to GestureListener, so we have open - // up a public set method to set it. - private int mLastButtonState = -1; - - public void setLastButtonState(int state) { - mLastButtonState = state; - } - - @Override - public boolean onSingleTapUp(MotionEvent e) { - // Single tap logic: - // We first see if it's a mouse event, and if it was right click by checking on - // @{code ListeningGestureDetector#mLastButtonState} - // If the selection manager is active, it gets first whack at handling tap - // events. Otherwise, tap events are routed to the target DocumentHolder. - if (Events.isMouseEvent(e) && mLastButtonState == MotionEvent.BUTTON_SECONDARY) { - mLastButtonState = -1; - return onRightClick(e); - } - - final MotionInputEvent event = MotionInputEvent.obtain(e, mRecView); - try { - boolean handled = mSelectionManager.onSingleTapUp(event); - - if (handled) { - return handled; - } - - // Give the DocumentHolder a crack at the event. - DocumentHolder holder = DirectoryFragment.this.getTarget(event); - if (holder != null) { - handled = holder.onSingleTapUp(e); - } - - return handled; - } finally { - event.recycle(); - } - } - - @Override - public void onLongPress(MotionEvent e) { - // Long-press events get routed directly to the selection manager. They can be - // changed to route through the DocumentHolder if necessary. - final MotionInputEvent event = MotionInputEvent.obtain(e, mRecView); - try { - mSelectionManager.onLongPress(event); - } finally { - event.recycle(); - } - } - - @Override - public boolean onDoubleTap(MotionEvent e) { - // Double-tap events are handled directly by the DirectoryFragment. They can be changed - // to route through the DocumentHolder if necessary. - final MotionInputEvent event = MotionInputEvent.obtain(e, mRecView); - return DirectoryFragment.this.onDoubleTap(event); - } - - public boolean onRightClick(MotionEvent e) { - return DirectoryFragment.this.onRightClick(e); - } - } - public static void showDirectory( FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) { create(fm, TYPE_NORMAL, root, doc, null, anim); diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/GestureListener.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/GestureListener.java new file mode 100644 index 0000000000000..1af26d0e70648 --- /dev/null +++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/GestureListener.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.documentsui.dirlist; + +import android.support.v7.widget.RecyclerView; +import android.view.GestureDetector; +import android.view.MotionEvent; + +import com.android.documentsui.Events; +import com.android.documentsui.Events.MotionInputEvent; + +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * The gesture listener for items in the directly list, interprets gestures, and sends the + * events to the target DocumentHolder, whence they are routed to the appropriate listener. + */ +final class GestureListener extends GestureDetector.SimpleOnGestureListener { + // From the RecyclerView, we get two events sent to + // ListeningGestureDetector#onInterceptTouchEvent on a mouse click; we first get an + // ACTION_DOWN Event for clicking on the mouse, and then an ACTION_UP event from releasing + // the mouse click. ACTION_UP event doesn't have information regarding the button (primary + // vs. secondary), so we have to save that somewhere first from ACTION_DOWN, and then reuse + // it later. The ACTION_DOWN event doesn't get forwarded to GestureListener, so we have open + // up a public set method to set it. + private int mLastButtonState = -1; + private MultiSelectManager mSelectionMgr; + private RecyclerView mRecView; + private Function mDocFinder; + private Predicate mDoubleTapHandler; + private Predicate mRightClickHandler; + + public GestureListener( + MultiSelectManager selectionMgr, + RecyclerView recView, + Function docFinder, + Predicate doubleTapHandler, + Predicate rightClickHandler) { + mSelectionMgr = selectionMgr; + mRecView = recView; + mDocFinder = docFinder; + mDoubleTapHandler = doubleTapHandler; + mRightClickHandler = rightClickHandler; + } + + public void setLastButtonState(int state) { + mLastButtonState = state; + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + // Single tap logic: + // We first see if it's a mouse event, and if it was right click by checking on + // @{code ListeningGestureDetector#mLastButtonState} + // If the selection manager is active, it gets first whack at handling tap + // events. Otherwise, tap events are routed to the target DocumentHolder. + if (Events.isMouseEvent(e) && mLastButtonState == MotionEvent.BUTTON_SECONDARY) { + mLastButtonState = -1; + return onRightClick(e); + } + + try (MotionInputEvent event = MotionInputEvent.obtain(e, mRecView)) { + boolean handled = mSelectionMgr.onSingleTapUp(event); + + if (handled) { + return handled; + } + + // Give the DocumentHolder a crack at the event. + DocumentHolder holder = mDocFinder.apply(event); + if (holder != null) { + handled = holder.onSingleTapUp(e); + } + + return handled; + } + } + + @Override + public void onLongPress(MotionEvent e) { + // Long-press events get routed directly to the selection manager. They can be + // changed to route through the DocumentHolder if necessary. + try (MotionInputEvent event = MotionInputEvent.obtain(e, mRecView)) { + mSelectionMgr.onLongPress(event); + } + } + + @Override + public boolean onDoubleTap(MotionEvent e) { + // Double-tap events are handled directly by the DirectoryFragment. They can be changed + // to route through the DocumentHolder if necessary. + + try (MotionInputEvent event = MotionInputEvent.obtain(e, mRecView)) { + return mDoubleTapHandler.test(event); + } + } + + public boolean onRightClick(MotionEvent e) { + try (MotionInputEvent event = MotionInputEvent.obtain(e, mRecView)) { + return mRightClickHandler.test(event); + } + } +} \ No newline at end of file diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/ListeningGestureDetector.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/ListeningGestureDetector.java index 96e15d97fa69e..50e595d39605d 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/ListeningGestureDetector.java +++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/ListeningGestureDetector.java @@ -26,7 +26,6 @@ import android.view.View; import android.view.View.OnTouchListener; import com.android.documentsui.Events; -import com.android.documentsui.dirlist.DirectoryFragment.GestureListener; // Previously we listened to events with one class, only to bounce them forward // to GestureDetector. We're still doing that here, but with a single class diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java index eeefac04e564e..e0fc5414fa921 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java +++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java @@ -65,39 +65,13 @@ public final class MultiSelectManager { private Range mRanger; private boolean mSingleSelect; - /** - * @param mode Selection single or multiple selection mode. - * @param initialSelection selection state probably preserved in external state. - */ - public MultiSelectManager( - final RecyclerView recyclerView, - DocumentsAdapter adapter, - @SelectionMode int mode, - @Nullable Selection initialSelection) { - - this(adapter, mode, initialSelection); - } - - /** - * Constructs a new instance with {@code adapter} and {@code helper}. - * @param runtimeSelectionEnvironment - * @hide - */ - @VisibleForTesting - MultiSelectManager( - DocumentsAdapter adapter, - @SelectionMode int mode, - @Nullable Selection initialSelection) { + public MultiSelectManager(DocumentsAdapter adapter, @SelectionMode int mode) { assert(adapter != null); mAdapter = adapter; mSingleSelect = mode == MODE_SINGLE; - if (initialSelection != null) { - mSelection.copyFrom(initialSelection); - } - mAdapter.registerAdapterDataObserver( new RecyclerView.AdapterDataObserver() { diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java index 9401da8fb5daf..7864e98e419ab 100644 --- a/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java @@ -46,10 +46,11 @@ public class MultiSelectManagerTest extends AndroidTestCase { private TestCallback mCallback; private TestDocumentsAdapter mAdapter; + @Override public void setUp() throws Exception { mCallback = new TestCallback(); mAdapter = new TestDocumentsAdapter(items); - mManager = new MultiSelectManager(mAdapter, MultiSelectManager.MODE_MULTIPLE, null); + mManager = new MultiSelectManager(mAdapter, MultiSelectManager.MODE_MULTIPLE); mManager.addCallback(mCallback); } @@ -173,7 +174,7 @@ public class MultiSelectManagerTest extends AndroidTestCase { } public void testSingleSelectMode() { - mManager = new MultiSelectManager(mAdapter, MultiSelectManager.MODE_SINGLE, null); + mManager = new MultiSelectManager(mAdapter, MultiSelectManager.MODE_SINGLE); mManager.addCallback(mCallback); longPress(20); tap(13); @@ -181,7 +182,7 @@ public class MultiSelectManagerTest extends AndroidTestCase { } public void testSingleSelectMode_ShiftTap() { - mManager = new MultiSelectManager(mAdapter, MultiSelectManager.MODE_SINGLE, null); + mManager = new MultiSelectManager(mAdapter, MultiSelectManager.MODE_SINGLE); mManager.addCallback(mCallback); longPress(13); shiftTap(20); @@ -228,7 +229,7 @@ public class MultiSelectManagerTest extends AndroidTestCase { } public void testRangeSelection_singleSelect() { - mManager = new MultiSelectManager(mAdapter, MultiSelectManager.MODE_SINGLE, null); + mManager = new MultiSelectManager(mAdapter, MultiSelectManager.MODE_SINGLE); mManager.addCallback(mCallback); mManager.startRangeSelection(11); mManager.snapRangeSelection(19);