From 8c555c684d2286a3dda7c4195d7a4e130708221d Mon Sep 17 00:00:00 2001 From: M66B Date: Sun, 8 Dec 2019 19:33:02 +0100 Subject: [PATCH] Updated AndroidX --- app/build.gradle | 15 +- .../recyclerview/selection/BandPredicate.java | 11 +- .../selection/BandSelectionHelper.java | 70 ++++--- .../selection/DefaultBandHost.java | 3 +- .../selection/DefaultSelectionTracker.java | 121 ++++++----- .../selection/DummyOnItemTouchListener.java | 43 ++++ .../recyclerview/selection/EventRouter.java | 69 ++++++ .../recyclerview/selection/FocusDelegate.java | 17 +- ...ureDetectorOnItemTouchListenerAdapter.java | 58 ++++++ .../recyclerview/selection/GestureRouter.java | 1 + .../selection/GestureSelectionHelper.java | 84 ++++---- .../recyclerview/selection/GridModel.java | 10 +- .../selection/ItemDetailsLookup.java | 14 +- .../recyclerview/selection/MotionEvents.java | 8 + .../selection/MouseInputHandler.java | 7 +- .../selection/OperationMonitor.java | 81 +++++-- .../PointerDragEventInterceptor.java | 27 ++- .../recyclerview/selection/Range.java | 14 +- .../recyclerview/selection/ResetManager.java | 101 +++++++++ .../recyclerview/selection/Resettable.java | 45 ++++ .../recyclerview/selection/Selection.java | 29 ++- .../selection/SelectionPredicates.java | 4 +- .../selection/SelectionTracker.java | 197 +++++++++++------- .../selection/StorageStrategy.java | 23 +- .../selection/ToolHandlerRegistry.java | 22 +- .../selection/TouchEventRouter.java | 113 ---------- .../selection/TouchInputHandler.java | 14 +- .../recyclerview/selection/package-info.java | 2 +- patches/recyclerview-selection.patch | 22 +- 29 files changed, 785 insertions(+), 440 deletions(-) create mode 100644 app/src/main/java/androidx/recyclerview/selection/DummyOnItemTouchListener.java create mode 100644 app/src/main/java/androidx/recyclerview/selection/EventRouter.java create mode 100644 app/src/main/java/androidx/recyclerview/selection/GestureDetectorOnItemTouchListenerAdapter.java create mode 100644 app/src/main/java/androidx/recyclerview/selection/ResetManager.java create mode 100644 app/src/main/java/androidx/recyclerview/selection/Resettable.java delete mode 100644 app/src/main/java/androidx/recyclerview/selection/TouchEventRouter.java diff --git a/app/build.gradle b/app/build.gradle index f712cc521c..44cbc82c35 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -181,17 +181,18 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) def core_version = "1.2.0-rc01" - def appcompat_version = "1.1.0" - def fragment_version = "1.2.0-rc02" + def appcompat_version = "1.2.0-alpha01" + def fragment_version = "1.2.0-rc03" def recyclerview_version = "1.1.0" - def coordinatorlayout_version = "1.1.0-rc01" + def coordinatorlayout_version = "1.1.0" def constraintlayout_version = "2.0.0-beta3" def material_version = "1.2.0-alpha02" - def browser_version = "1.2.0-beta01" + def browser_version = "1.2.0-rc01" def lbm_version = "1.0.0" def swiperefresh_version = "1.0.0" def documentfile_version = "1.0.1" - def lifecycle_version = "2.2.0-rc02" + def lifecycle_version = "2.2.0-rc03" + def sqlite_version = "2.1.0-beta01" def room_version = "2.2.2" def paging_version = "2.1.0" def preference_version = "1.1.0" @@ -226,7 +227,7 @@ dependencies { // https://mvnrepository.com/artifact/androidx.recyclerview/recyclerview // https://mvnrepository.com/artifact/androidx.recyclerview/recyclerview-selection implementation "androidx.recyclerview:recyclerview:$recyclerview_version" - //implementation "androidx.recyclerview:recyclerview-selection:1.1.0-alpha06" + //implementation "androidx.recyclerview:recyclerview-selection:1.1.0-beta01" // https://mvnrepository.com/artifact/androidx.coordinatorlayout/coordinatorlayout implementation "androidx.coordinatorlayout:coordinatorlayout:$coordinatorlayout_version" @@ -260,7 +261,7 @@ dependencies { implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-common:$room_version" // because of exclude // https://mvnrepository.com/artifact/androidx.sqlite/sqlite-framework - implementation "androidx.sqlite:sqlite-framework:2.1.0-alpha01" // because of exclude + implementation "androidx.sqlite:sqlite-framework:$sqlite_version" // because of exclude annotationProcessor "androidx.room:room-compiler:$room_version" // https://mvnrepository.com/artifact/androidx.paging/paging-runtime diff --git a/app/src/main/java/androidx/recyclerview/selection/BandPredicate.java b/app/src/main/java/androidx/recyclerview/selection/BandPredicate.java index 1ee3d73801..3ec4ccb8ac 100644 --- a/app/src/main/java/androidx/recyclerview/selection/BandPredicate.java +++ b/app/src/main/java/androidx/recyclerview/selection/BandPredicate.java @@ -40,7 +40,7 @@ public abstract class BandPredicate { /** * @return true if band selection can be initiated in response to the {@link MotionEvent}. */ - public abstract boolean canInitiate(MotionEvent e); + public abstract boolean canInitiate(@NonNull MotionEvent e); @SuppressWarnings("WeakerAccess") /* synthetic access */ static boolean hasSupportedLayoutManager(@NonNull RecyclerView recyclerView) { @@ -107,16 +107,16 @@ public abstract class BandPredicate { public static final class NonDraggableArea extends BandPredicate { private final RecyclerView mRecyclerView; - private final ItemDetailsLookup mDetailsLookup; + private final ItemDetailsLookup mDetailsLookup; /** * Creates a new instance. * - * @param recyclerView the owner RecyclerView + * @param recyclerView the owner RecyclerView * @param detailsLookup provides access to item details. */ public NonDraggableArea( - @NonNull RecyclerView recyclerView, @NonNull ItemDetailsLookup detailsLookup) { + @NonNull RecyclerView recyclerView, @NonNull ItemDetailsLookup detailsLookup) { checkArgument(recyclerView != null); checkArgument(detailsLookup != null); @@ -132,7 +132,8 @@ public abstract class BandPredicate { return false; } - @Nullable ItemDetailsLookup.ItemDetails details = mDetailsLookup.getItemDetails(e); + @Nullable ItemDetailsLookup.ItemDetails details = + mDetailsLookup.getItemDetails(e); return (details == null) || !details.inDragRegion(e); } } diff --git a/app/src/main/java/androidx/recyclerview/selection/BandSelectionHelper.java b/app/src/main/java/androidx/recyclerview/selection/BandSelectionHelper.java index 495b63f2d0..591d1274ac 100644 --- a/app/src/main/java/androidx/recyclerview/selection/BandSelectionHelper.java +++ b/app/src/main/java/androidx/recyclerview/selection/BandSelectionHelper.java @@ -28,7 +28,6 @@ import android.view.MotionEvent; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener; @@ -48,17 +47,14 @@ import java.util.Set; * the user interacts with items using their pointer (and the band). Selectable items that intersect * with the band, both on and off screen, are selected on pointer up. * - * @see SelectionTracker.Builder#withPointerTooltypes(int...) for details on the specific - * tooltypes routed to this helper. - * * @param Selection key type. @see {@link StorageStrategy} for supported types. */ -class BandSelectionHelper implements OnItemTouchListener { +class BandSelectionHelper implements OnItemTouchListener, Resettable { static final String TAG = "BandSelectionHelper"; static final boolean DEBUG = false; - private final BandHost mHost; + private final BandHost mHost; private final ItemKeyProvider mKeyProvider; @SuppressWarnings("WeakerAccess") /* synthetic access */ final SelectionTracker mSelectionTracker; @@ -66,17 +62,17 @@ class BandSelectionHelper implements OnItemTouchListener { private final FocusDelegate mFocusDelegate; private final OperationMonitor mLock; private final AutoScroller mScroller; - private final GridModel.SelectionObserver mGridObserver; + private final GridModel.SelectionObserver mGridObserver; private @Nullable Point mCurrentPosition; private @Nullable Point mOrigin; - private @Nullable GridModel mModel; + private @Nullable GridModel mModel; /** * See {@link BandSelectionHelper#create}. */ BandSelectionHelper( - @NonNull BandHost host, + @NonNull BandHost host, @NonNull AutoScroller scroller, @NonNull ItemKeyProvider keyProvider, @NonNull SelectionTracker selectionTracker, @@ -122,7 +118,7 @@ class BandSelectionHelper implements OnItemTouchListener { * * @return new BandSelectionHelper instance. */ - static BandSelectionHelper create( + static BandSelectionHelper create( @NonNull RecyclerView recyclerView, @NonNull AutoScroller scroller, @DrawableRes int bandOverlayId, @@ -143,24 +139,23 @@ class BandSelectionHelper implements OnItemTouchListener { lock); } - @VisibleForTesting - boolean isActive() { - boolean active = mModel != null; - if (DEBUG && active) { - mLock.checkStarted(); - } - return active; + private boolean isActive() { + boolean started = mModel != null; + if (DEBUG) mLock.checkStarted(started); + return started; } /** * Clients must call reset when there are any material changes to the layout of items * in RecyclerView. */ - void reset() { + @Override + public void reset() { if (!isActive()) { + if (DEBUG) Log.d(TAG, "Ignoring reset request, not active."); return; } - + if (DEBUG) Log.d(TAG, "Handling reset request."); mHost.hideBand(); if (mModel != null) { mModel.stopCapturing(); @@ -171,11 +166,15 @@ class BandSelectionHelper implements OnItemTouchListener { mOrigin = null; mScroller.reset(); - mLock.stop(); + // mLock is reset by reset manager. } - @VisibleForTesting - boolean shouldStart(@NonNull MotionEvent e) { + @Override + public boolean isResetRequired() { + return isActive(); + } + + private boolean shouldStart(@NonNull MotionEvent e) { // b/30146357 && b/23793622. onInterceptTouchEvent does not dispatch events to onTouchEvent // unless the event is != ACTION_DOWN. Thus, we need to actually start band selection when // mouse moves. @@ -185,12 +184,8 @@ class BandSelectionHelper implements OnItemTouchListener { && !isActive(); } - @VisibleForTesting - boolean shouldStop(@NonNull MotionEvent e) { - return isActive() - && (MotionEvents.isActionUp(e) - || MotionEvents.isActionPointerUp(e) - || MotionEvents.isActionCancel(e)); + private boolean shouldStop(@NonNull MotionEvent e) { + return isActive() && MotionEvents.isActionUp(e); } @Override @@ -242,7 +237,9 @@ class BandSelectionHelper implements OnItemTouchListener { * Starts band select by adding the drawable to the RecyclerView's overlay. */ private void startBandSelect(@NonNull MotionEvent e) { - checkState(!isActive()); + if (DEBUG) { + checkState(!isActive()); + } if (!MotionEvents.isCtrlKeyPressed(e)) { mSelectionTracker.clearSelection(); @@ -303,7 +300,18 @@ class BandSelectionHelper implements OnItemTouchListener { } mSelectionTracker.mergeProvisionalSelection(); - reset(); + mLock.stop(); + + mHost.hideBand(); + if (mModel != null) { + mModel.stopCapturing(); + mModel.onDestroy(); + } + + mModel = null; + mOrigin = null; + + mScroller.reset(); } /** @@ -348,8 +356,6 @@ class BandSelectionHelper implements OnItemTouchListener { /** * Add a listener to be notified on scroll events. - * - * @param listener */ abstract void addOnScrollListener(@NonNull OnScrollListener listener); } diff --git a/app/src/main/java/androidx/recyclerview/selection/DefaultBandHost.java b/app/src/main/java/androidx/recyclerview/selection/DefaultBandHost.java index bb1a034de5..3b6e256489 100644 --- a/app/src/main/java/androidx/recyclerview/selection/DefaultBandHost.java +++ b/app/src/main/java/androidx/recyclerview/selection/DefaultBandHost.java @@ -26,6 +26,7 @@ import android.view.View; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -53,7 +54,7 @@ final class DefaultBandHost extends GridModel.GridHost { checkArgument(recyclerView != null); mRecyclerView = recyclerView; - mBand = mRecyclerView.getContext().getResources().getDrawable(bandOverlayId); + mBand = ContextCompat.getDrawable(mRecyclerView.getContext(), bandOverlayId); checkArgument(mBand != null); checkArgument(keyProvider != null); diff --git a/app/src/main/java/androidx/recyclerview/selection/DefaultSelectionTracker.java b/app/src/main/java/androidx/recyclerview/selection/DefaultSelectionTracker.java index aa523514bc..61985cf8d4 100644 --- a/app/src/main/java/androidx/recyclerview/selection/DefaultSelectionTracker.java +++ b/app/src/main/java/androidx/recyclerview/selection/DefaultSelectionTracker.java @@ -46,17 +46,17 @@ import java.util.Set; * {@link SelectionPredicate#canSelectMultiple()}. * * @param Selection key type. @see {@link StorageStrategy} for supported types. - * * @hide */ @RestrictTo(LIBRARY) -public class DefaultSelectionTracker extends SelectionTracker { +@SuppressWarnings("unchecked") +public class DefaultSelectionTracker extends SelectionTracker implements Resettable { private static final String TAG = "DefaultSelectionTracker"; private static final String EXTRA_SELECTION_PREFIX = "androidx.recyclerview.selection"; private final Selection mSelection = new Selection<>(); - private final List mObservers = new ArrayList<>(1); + private final List> mObservers = new ArrayList<>(1); private final ItemKeyProvider mKeyProvider; private final SelectionPredicate mSelectionPredicate; private final StorageStrategy mStorage; @@ -70,16 +70,16 @@ public class DefaultSelectionTracker extends SelectionTracker { /** * Creates a new instance. * - * @param selectionId A unique string identifying this selection in the context - * of the activity or fragment. - * @param keyProvider client supplied class providing access to stable ids. + * @param selectionId A unique string identifying this selection in the context + * of the activity or fragment. + * @param keyProvider client supplied class providing access to stable ids. * @param selectionPredicate A predicate allowing the client to disallow selection - * @param storage Strategy for storing typed selection in bundle. + * @param storage Strategy for storing typed selection in bundle. */ public DefaultSelectionTracker( @NonNull String selectionId, - @NonNull ItemKeyProvider keyProvider, - @NonNull SelectionPredicate selectionPredicate, + @NonNull ItemKeyProvider keyProvider, + @NonNull SelectionPredicate selectionPredicate, @NonNull StorageStrategy storage) { checkArgument(selectionId != null); @@ -101,23 +101,26 @@ public class DefaultSelectionTracker extends SelectionTracker { } @Override - public void addObserver(@NonNull SelectionObserver callback) { + public void addObserver(@NonNull SelectionObserver callback) { checkArgument(callback != null); mObservers.add(callback); } + /** + * @return true if there is a primary or previsional selection. + */ @Override public boolean hasSelection() { return !mSelection.isEmpty(); } @Override - public Selection getSelection() { + public @NonNull Selection getSelection() { return mSelection; } @Override - public void copySelection(@NonNull MutableSelection dest) { + public void copySelection(@NonNull MutableSelection dest) { dest.copyFrom(mSelection); } @@ -127,7 +130,7 @@ public class DefaultSelectionTracker extends SelectionTracker { } @Override - protected void restoreSelection(@NonNull Selection other) { + protected void restoreSelection(@NonNull Selection other) { checkArgument(other != null); setItemsSelectedQuietly(other.mSelection, true); // NOTE: We intentionally don't restore provisional selection. It's provisional. @@ -143,7 +146,7 @@ public class DefaultSelectionTracker extends SelectionTracker { private boolean setItemsSelectedQuietly(@NonNull Iterable keys, boolean selected) { boolean changed = false; - for (K key: keys) { + for (K key : keys) { boolean itemChanged = selected ? canSetState(key, true) && mSelection.add(key) : canSetState(key, false) && mSelection.remove(key); @@ -158,11 +161,15 @@ public class DefaultSelectionTracker extends SelectionTracker { @Override public boolean clearSelection() { if (!hasSelection()) { + if (DEBUG) Log.d(TAG, "Ignoring clearSelection request. No selection."); return false; } + if (DEBUG) Log.d(TAG, "Handling clearSelection request."); clearProvisionalSelection(); clearPrimarySelection(); + notifySelectionCleared(); + return true; } @@ -171,7 +178,7 @@ public class DefaultSelectionTracker extends SelectionTracker { return; } - Selection prev = clearSelectionQuietly(); + Selection prev = clearSelectionQuietly(); notifySelectionCleared(prev); notifySelectionChanged(); } @@ -181,10 +188,10 @@ public class DefaultSelectionTracker extends SelectionTracker { * Returns items in previous selection. Callers are responsible for notifying * listeners about changes. */ - private Selection clearSelectionQuietly() { + private Selection clearSelectionQuietly() { mRange = null; - MutableSelection prevSelection = new MutableSelection(); + MutableSelection prevSelection = new MutableSelection(); if (hasSelection()) { copySelection(prevSelection); mSelection.clear(); @@ -193,6 +200,18 @@ public class DefaultSelectionTracker extends SelectionTracker { return prevSelection; } + @Override + public void reset() { + if (DEBUG) Log.d(TAG, "Received reset request."); + clearSelection(); + mRange = null; + } + + @Override + public boolean isResetRequired() { + return hasSelection() || isRangeActive(); + } + @Override public boolean select(@NonNull K key) { checkArgument(key != null); @@ -208,7 +227,7 @@ public class DefaultSelectionTracker extends SelectionTracker { // Enforce single selection policy. if (mSingleSelect && hasSelection()) { - Selection prev = clearSelectionQuietly(); + Selection prev = clearSelectionQuietly(); notifySelectionCleared(prev); } @@ -277,8 +296,10 @@ public class DefaultSelectionTracker extends SelectionTracker { return; } - if (DEBUG) Log.i(TAG, "Extending provision range to position: " + position); - checkState(isRangeActive(), "Range start point not set."); + if (DEBUG) { + Log.i(TAG, "Extending provision range to position: " + position); + checkState(isRangeActive(), "Range start point not set."); + } extendRange(position, Range.TYPE_PROVISIONAL); } @@ -291,13 +312,23 @@ public class DefaultSelectionTracker extends SelectionTracker { * point before calling on {@link #endRange()}. * * @param position The new end position for the selection range. - * @param type The type of selection the range should utilize. + * @param type The type of selection the range should utilize. */ private void extendRange(int position, @RangeType int type) { - checkState(isRangeActive(), "Range start point not set."); + if (!isRangeActive()) { + Log.e(TAG, "Ignoring attempt to extend unestablished range. Ignoring."); + if (DEBUG) { + throw new IllegalStateException("Attempted to extend unestablished range."); + } + return; + } if (position == RecyclerView.NO_POSITION) { - Log.w(TAG, "Invalid position: Cannot extend selection to: " + position); + Log.w(TAG, "Ignoring attempt to extend range to invalid position: " + position); + if (DEBUG) { + throw new IllegalStateException( + "Attempting to extend range to invalid position: " + position); + } return; } @@ -316,7 +347,7 @@ public class DefaultSelectionTracker extends SelectionTracker { } Map delta = mSelection.setProvisionalSelection(newSelection); - for (Map.Entry entry: delta.entrySet()) { + for (Map.Entry entry : delta.entrySet()) { notifyItemStateChanged(entry.getKey(), entry.getValue()); } @@ -348,20 +379,16 @@ public class DefaultSelectionTracker extends SelectionTracker { return mRange != null; } - boolean isOverlapping(int position, int count) { - return (mRange != null && mRange.isOverlapping(position, count)); - } - private boolean canSetState(@NonNull K key, boolean nextState) { return mSelectionPredicate.canSetStateForKey(key, nextState); } @Override - protected AdapterDataObserver getAdapterDataObserver() { + protected @NonNull AdapterDataObserver getAdapterDataObserver() { return mAdapterObserver; } - @SuppressWarnings("WeakerAccess") /* synthetic access */ + @SuppressWarnings({"WeakerAccess", "unchecked"}) /* synthetic access */ void onDataSetChanged() { mSelection.clearProvisionalSelection(); @@ -409,11 +436,17 @@ public class DefaultSelectionTracker extends SelectionTracker { } } + private void notifySelectionCleared() { + for (SelectionObserver observer : mObservers) { + observer.onSelectionCleared(); + } + } + private void notifySelectionCleared(@NonNull Selection selection) { - for (K key: selection.mSelection) { + for (K key : selection.mSelection) { notifyItemStateChanged(key, false); } - for (K key: selection.mProvisionalSelection) { + for (K key : selection.mProvisionalSelection) { notifyItemStateChanged(key, false); } } @@ -445,19 +478,6 @@ public class DefaultSelectionTracker extends SelectionTracker { } } - private void updateForRange(int begin, int end, boolean selected, @RangeType int type) { - switch (type) { - case Range.TYPE_PRIMARY: - updateForRegularRange(begin, end, selected); - break; - case Range.TYPE_PROVISIONAL: - updateForProvisionalRange(begin, end, selected); - break; - default: - throw new IllegalArgumentException("Invalid range type: " + type); - } - } - @SuppressWarnings("WeakerAccess") /* synthetic access */ void updateForRegularRange(int begin, int end, boolean selected) { checkArgument(end >= begin); @@ -514,7 +534,6 @@ public class DefaultSelectionTracker extends SelectionTracker { } @Override - @SuppressWarnings("unchecked") public final void onSaveInstanceState(@NonNull Bundle state) { if (mSelection.isEmpty()) { return; @@ -582,21 +601,17 @@ public class DefaultSelectionTracker extends SelectionTracker { @Override public void onItemRangeInserted(int startPosition, int itemCount) { - if (mSelectionTracker.isOverlapping(startPosition, itemCount)) - mSelectionTracker.endRange(); + mSelectionTracker.endRange(); } @Override public void onItemRangeRemoved(int startPosition, int itemCount) { - if (mSelectionTracker.isOverlapping(startPosition, itemCount)) - mSelectionTracker.endRange(); + mSelectionTracker.endRange(); } @Override public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { - if (mSelectionTracker.isOverlapping(fromPosition, itemCount) || - mSelectionTracker.isOverlapping(toPosition, itemCount)) - mSelectionTracker.endRange(); + mSelectionTracker.endRange(); } } } diff --git a/app/src/main/java/androidx/recyclerview/selection/DummyOnItemTouchListener.java b/app/src/main/java/androidx/recyclerview/selection/DummyOnItemTouchListener.java new file mode 100644 index 0000000000..5880d9781a --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/selection/DummyOnItemTouchListener.java @@ -0,0 +1,43 @@ +/* + * Copyright 2019 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 androidx.recyclerview.selection; + +import android.view.MotionEvent; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +/** + * No-op implementation of OnItemTouchListener suitable for use as a default + * handler w/ ToolHandlerRegistery, or in tests. + */ +final class DummyOnItemTouchListener implements RecyclerView.OnItemTouchListener { + @Override + public boolean onInterceptTouchEvent( + @NonNull RecyclerView unused, @NonNull MotionEvent e) { + return false; + } + + @Override + public void onTouchEvent( + @NonNull RecyclerView unused, @NonNull MotionEvent e) { + } + + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + } +} diff --git a/app/src/main/java/androidx/recyclerview/selection/EventRouter.java b/app/src/main/java/androidx/recyclerview/selection/EventRouter.java new file mode 100644 index 0000000000..c83523ea1b --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/selection/EventRouter.java @@ -0,0 +1,69 @@ +/* + * Copyright 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 androidx.recyclerview.selection; + +import static androidx.core.util.Preconditions.checkArgument; + +import android.view.MotionEvent; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener; + +/** + * A class responsible for routing MotionEvents to tool-type specific handlers. + * Individual tool-type specific handlers are added after the class is constructed. + * + *

+ * EventRouter takes its name from + * {@link RecyclerView#addOnItemTouchListener(OnItemTouchListener)}. Despite "Touch" + * being in the name, it receives MotionEvents for all types of tools. + */ +final class EventRouter implements OnItemTouchListener { + + private final ToolHandlerRegistry mDelegates; + + EventRouter() { + mDelegates = new ToolHandlerRegistry<>(new DummyOnItemTouchListener()); + } + + /** + * @param toolType See MotionEvent for details on available types. + * @param delegate An {@link OnItemTouchListener} to receive events + * of {@code toolType}. + */ + void set(int toolType, @NonNull OnItemTouchListener delegate) { + checkArgument(delegate != null); + + mDelegates.set(toolType, delegate); + } + + @Override + public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { + return mDelegates.get(e).onInterceptTouchEvent(rv, e); + } + + @Override + public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { + mDelegates.get(e).onTouchEvent(rv, e); + } + + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + // TODO(b/139141511): Handle onRequestDisallowInterceptTouchEvent. + } +} diff --git a/app/src/main/java/androidx/recyclerview/selection/FocusDelegate.java b/app/src/main/java/androidx/recyclerview/selection/FocusDelegate.java index ca69c85214..9bc31ec0db 100644 --- a/app/src/main/java/androidx/recyclerview/selection/FocusDelegate.java +++ b/app/src/main/java/androidx/recyclerview/selection/FocusDelegate.java @@ -61,7 +61,22 @@ public abstract class FocusDelegate { public abstract boolean hasFocusedItem(); /** - * @return the position of the currently focused item, if any. + * Returns the position of the currently focused item, or + * {@link RecyclerView#NO_POSITION} if nothing is focused. + * + *

You must implement this feature if you intend your app + * to work well with mouse and keyboard. Selection + * ranges are inferred from focused item when there is + * no explicit last-selected item. + * + *

You can manage and advance focus using keyboard arrows, + * reflecting this state visibly in the view item. + * Use can then press shift, then click another item with + * their mouse to select all items between the focused + * item and the clicked item. + * + * @return the position of the currently focused item, + * or {@code RecyclerView#NO_POSITION} if none. */ public abstract int getFocusedPosition(); diff --git a/app/src/main/java/androidx/recyclerview/selection/GestureDetectorOnItemTouchListenerAdapter.java b/app/src/main/java/androidx/recyclerview/selection/GestureDetectorOnItemTouchListenerAdapter.java new file mode 100644 index 0000000000..5cf2c74188 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/selection/GestureDetectorOnItemTouchListenerAdapter.java @@ -0,0 +1,58 @@ +/* + * Copyright 2019 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 androidx.recyclerview.selection; + +import static androidx.core.util.Preconditions.checkArgument; + +import android.view.GestureDetector; +import android.view.MotionEvent; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +/** + * Class allowing GestureDetector to listen directly to RecyclerView touch events. + */ +final class GestureDetectorOnItemTouchListenerAdapter implements RecyclerView.OnItemTouchListener { + + private final GestureDetector mDetector; + + GestureDetectorOnItemTouchListenerAdapter(@NonNull GestureDetector detector) { + checkArgument(detector != null); + + mDetector = detector; + } + + @Override + public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { + // While the idea of "intercepting" an event stream isn't consistent + // with the world-view of GestureDetector, failure to return true here + // resulted in a bug where a context menu shown on an item view was not + // visible...despite returning reporting that the menu was shown. + // See b/143494310 for further details. + return mDetector.onTouchEvent(e); + } + + @Override + public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { + } + + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + } +} + diff --git a/app/src/main/java/androidx/recyclerview/selection/GestureRouter.java b/app/src/main/java/androidx/recyclerview/selection/GestureRouter.java index 797f32d991..3c25415326 100644 --- a/app/src/main/java/androidx/recyclerview/selection/GestureRouter.java +++ b/app/src/main/java/androidx/recyclerview/selection/GestureRouter.java @@ -43,6 +43,7 @@ final class GestureRouter mDelegates = new ToolHandlerRegistry<>(defaultDelegate); } + @SuppressWarnings("unchecked") GestureRouter() { this((T) new SimpleOnGestureListener()); } diff --git a/app/src/main/java/androidx/recyclerview/selection/GestureSelectionHelper.java b/app/src/main/java/androidx/recyclerview/selection/GestureSelectionHelper.java index 8caf86d239..046a8afa89 100644 --- a/app/src/main/java/androidx/recyclerview/selection/GestureSelectionHelper.java +++ b/app/src/main/java/androidx/recyclerview/selection/GestureSelectionHelper.java @@ -17,7 +17,8 @@ package androidx.recyclerview.selection; import static androidx.core.util.Preconditions.checkArgument; -import static androidx.core.util.Preconditions.checkState; +import static androidx.recyclerview.selection.Shared.DEBUG; +import static androidx.recyclerview.selection.Shared.VERBOSE; import android.util.Log; import android.view.MotionEvent; @@ -36,7 +37,7 @@ import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener; * when used in conjunction with RecyclerView and other classes in the ReyclerView * selection support package. */ -final class GestureSelectionHelper implements OnItemTouchListener { +final class GestureSelectionHelper implements OnItemTouchListener, Resettable { private static final String TAG = "GestureSelectionHelper"; @@ -76,16 +77,19 @@ final class GestureSelectionHelper implements OnItemTouchListener { * Explicitly kicks off a gesture multi-select. */ void start() { - checkState(!mStarted); - // Partner code in MotionInputHandler ensures items // are selected and range anchor initialized prior to // start being called. // Verify the truth of that statement here // to make the implicit coupling less of a time bomb. - checkState(mSelectionMgr.isRangeActive()); - - mLock.checkStopped(); + if (mStarted) { + if (DEBUG) { + Log.e(TAG, "Attempting to start, but state is already=started."); + throw new IllegalStateException( + "Attempting to start, but state is already=started."); + } + return; + } mStarted = true; mLock.start(); @@ -94,15 +98,20 @@ final class GestureSelectionHelper implements OnItemTouchListener { @Override /** @hide */ public boolean onInterceptTouchEvent(@NonNull RecyclerView unused, @NonNull MotionEvent e) { + // MotionEvents that aren't ACTION_DOWN are only ever passed to either onInterceptTouchEvent + // or onTouchEvent; never to both, so events delivered to this method are effectively + // lost if we don't act on them in this method. + // // TODO(b/132447183): For some reason we're not receiving an ACTION_UP // event after a > long-press NOT followed by a ACTION_MOVE < event. if (mStarted) { - handleTouch(e); + onTouchEvent(unused, e); } + // ACTION_CANCEL is associated with "TOOL_TYPE_UNKNOWN" and + // is handled in ResetManager. switch (e.getActionMasked()) { case MotionEvent.ACTION_MOVE: - case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: return mStarted; default: @@ -113,27 +122,11 @@ final class GestureSelectionHelper implements OnItemTouchListener { @Override /** @hide */ public void onTouchEvent(@NonNull RecyclerView unused, @NonNull MotionEvent e) { - // See handleTouch(MotionEvent) javadoc for explanation as to why this is correct. - handleTouch(e); - } + if (!mStarted) { + if (VERBOSE) Log.i(TAG, "Ignoring input event. Not started."); + return; + } - /** - * If selection has started, will handle all appropriate types of MotionEvents and will return - * true if this OnItemTouchListener should start intercepting the rest of the MotionEvents. - * - *

This code, and the fact that this method is used by both OnInterceptTouchEvent and - * OnTouchEvent, is correct and valid because: - *

    - *
  1. MotionEvents that aren't ACTION_DOWN are only ever passed to either onInterceptTouchEvent - * or onTouchEvent; never to both. The MotionEvents we are handling in this method are not - * ACTION_DOWN, and therefore, its appropriate that both the onInterceptTouchEvent and - * onTouchEvent code paths cross this method. - *
  2. This method returns true when we want to intercept MotionEvents. OnInterceptTouchEvent - * uses that information to determine its own return, and OnMotionEvent doesn't have a return - * so this methods return value is irrelevant to it. - *
- */ - private void handleTouch(MotionEvent e) { if (!mSelectionMgr.isRangeActive()) { Log.e(TAG, "Internal state of GestureSelectionHelper out of sync w/ SelectionTracker " @@ -141,6 +134,8 @@ final class GestureSelectionHelper implements OnItemTouchListener { endSelection(); } + // ACTION_CANCEL is associated with "TOOL_TYPE_UNKNOWN" and + // is handled in ResetManager. switch (e.getActionMasked()) { case MotionEvent.ACTION_MOVE: handleMoveEvent(e); @@ -148,9 +143,6 @@ final class GestureSelectionHelper implements OnItemTouchListener { case MotionEvent.ACTION_UP: handleUpEvent(); break; - case MotionEvent.ACTION_CANCEL: - handleCancelEvent(); - break; } } @@ -167,17 +159,22 @@ final class GestureSelectionHelper implements OnItemTouchListener { endSelection(); } - // Called when ACTION_CANCEL event is to be handled. - // This means this gesture selection is aborted, so reset everything and abandon provisional - // selection. - private void handleCancelEvent() { - mSelectionMgr.clearProvisionalSelection(); - endSelection(); + /** + * Immediately "Stops" active gesture selection, and resets all related state. + */ + @Override + public void reset() { + if (DEBUG) Log.d(TAG, "Received reset request."); + mStarted = false; + mScroller.reset(); + } + + @Override + public boolean isResetRequired() { + return mStarted; } private void endSelection() { - checkState(mStarted); - mStarted = false; mScroller.reset(); mLock.stop(); @@ -186,6 +183,11 @@ final class GestureSelectionHelper implements OnItemTouchListener { // Call when an intercepted ACTION_MOVE event is passed down. // At this point, we are sure user wants to gesture multi-select. private void handleMoveEvent(@NonNull MotionEvent e) { + if (!mStarted) { + Log.e(TAG, "Received event while not started."); + if (DEBUG) throw new IllegalStateException("Received event while not started."); + } + int lastGlidedItemPos = mView.getLastGlidedItemPosition(e); if (mSelectionPredicate.canSetStateAtPosition(lastGlidedItemPos, true)) { extendSelection(lastGlidedItemPos); @@ -283,7 +285,7 @@ final class GestureSelectionHelper implements OnItemTouchListener { // of items in the adapter. Using the adapter is the for sure way to get the actual last // item position. final float inboundY = getInboundY(mRecyclerView.getHeight(), e.getY()); - return (pastLastItem) ? mRecyclerView.getAdapter().getItemCount() - 1 + return pastLastItem ? mRecyclerView.getAdapter().getItemCount() - 1 : mRecyclerView.getChildAdapterPosition( mRecyclerView.findChildViewUnder(e.getX(), inboundY)); } diff --git a/app/src/main/java/androidx/recyclerview/selection/GridModel.java b/app/src/main/java/androidx/recyclerview/selection/GridModel.java index 17958ad55a..8453b41d08 100644 --- a/app/src/main/java/androidx/recyclerview/selection/GridModel.java +++ b/app/src/main/java/androidx/recyclerview/selection/GridModel.java @@ -63,7 +63,7 @@ final class GridModel { private final ItemKeyProvider mKeyProvider; private final SelectionPredicate mSelectionPredicate; - private final List mOnSelectionChangedListeners = new ArrayList<>(); + private final List> mOnSelectionChangedListeners = new ArrayList<>(); // Map from the x-value of the left side of a SparseBooleanArray of adapter positions, keyed // by their y-offset. For example, if the first column of the view starts at an x-value of 5, @@ -101,8 +101,9 @@ final class GridModel { private final OnScrollListener mScrollListener; + @SuppressWarnings("unchecked") GridModel( - GridHost host, + GridHost host, ItemKeyProvider keyProvider, SelectionPredicate selectionPredicate) { @@ -284,8 +285,9 @@ final class GridModel { * mSelection, so computeCurrentSelection() should be called before this * function. */ + @SuppressWarnings("unchecked") private void notifySelectionChanged() { - for (SelectionObserver listener : mOnSelectionChangedListeners) { + for (SelectionObserver listener : mOnSelectionChangedListeners) { listener.onSelectionChanged(mSelection); } } @@ -401,7 +403,7 @@ final class GridModel { abstract void onSelectionChanged(Set updatedSelection); } - void addOnSelectionChangedListener(SelectionObserver listener) { + void addOnSelectionChangedListener(SelectionObserver listener) { mOnSelectionChangedListeners.add(listener); } diff --git a/app/src/main/java/androidx/recyclerview/selection/ItemDetailsLookup.java b/app/src/main/java/androidx/recyclerview/selection/ItemDetailsLookup.java index 002849c7ce..1cdf135203 100644 --- a/app/src/main/java/androidx/recyclerview/selection/ItemDetailsLookup.java +++ b/app/src/main/java/androidx/recyclerview/selection/ItemDetailsLookup.java @@ -44,10 +44,10 @@ import androidx.recyclerview.widget.RecyclerView; * mRecyclerView = recyclerView; * } * - * public ItemDetails getItemDetails(MotionEvent e) { - * View view = mRecView.findChildViewUnder(e.getX(), e.getY()); + * public @Nullable ItemDetails getItemDetails(@NonNull MotionEvent e) { + * View view = mRecyclerView.findChildViewUnder(e.getX(), e.getY()); * if (view != null) { - * ViewHolder holder = mRecView.getChildViewHolder(view); + * ViewHolder holder = mRecyclerView.getChildViewHolder(view); * if (holder instanceof MyHolder) { * return ((MyHolder) holder).getItemDetails(); * } @@ -110,10 +110,6 @@ public abstract class ItemDetailsLookup { return item != null && item.getSelectionKey() != null; } - private static boolean hasPosition(@Nullable ItemDetails item) { - return item != null && item.getPosition() != RecyclerView.NO_POSITION; - } - /** * @return the ItemDetails for the item under the event, or null. */ @@ -241,10 +237,10 @@ public abstract class ItemDetailsLookup { @Override public boolean equals(@Nullable Object obj) { return (obj instanceof ItemDetails) - && isEqualTo((ItemDetails) obj); + && isEqualTo((ItemDetails) obj); } - private boolean isEqualTo(@NonNull ItemDetails other) { + private boolean isEqualTo(@NonNull ItemDetails other) { K key = getSelectionKey(); boolean sameKeys = false; if (key == null) { diff --git a/app/src/main/java/androidx/recyclerview/selection/MotionEvents.java b/app/src/main/java/androidx/recyclerview/selection/MotionEvents.java index fbc4014b37..aac3c0674d 100644 --- a/app/src/main/java/androidx/recyclerview/selection/MotionEvents.java +++ b/app/src/main/java/androidx/recyclerview/selection/MotionEvents.java @@ -33,6 +33,14 @@ final class MotionEvents { return e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE; } + static boolean isFingerEvent(@NonNull MotionEvent e) { + return e.getToolType(0) == MotionEvent.TOOL_TYPE_FINGER; + } + + static boolean isActionDown(@NonNull MotionEvent e) { + return e.getActionMasked() == MotionEvent.ACTION_DOWN; + } + static boolean isActionMove(@NonNull MotionEvent e) { return e.getActionMasked() == MotionEvent.ACTION_MOVE; } diff --git a/app/src/main/java/androidx/recyclerview/selection/MouseInputHandler.java b/app/src/main/java/androidx/recyclerview/selection/MouseInputHandler.java index e4f4548db9..db3f43486b 100644 --- a/app/src/main/java/androidx/recyclerview/selection/MouseInputHandler.java +++ b/app/src/main/java/androidx/recyclerview/selection/MouseInputHandler.java @@ -17,7 +17,6 @@ package androidx.recyclerview.selection; import static androidx.core.util.Preconditions.checkArgument; -import static androidx.core.util.Preconditions.checkState; import static androidx.recyclerview.selection.Shared.DEBUG; import static androidx.recyclerview.selection.Shared.VERBOSE; @@ -123,7 +122,11 @@ final class MouseInputHandler extends MotionInputHandler { // tap on an item when there is an existing selection. We could extend // a selection, we could clear selection (then launch) private void onItemClick(@NonNull MotionEvent e, @NonNull ItemDetails item) { - checkState(mSelectionTracker.hasSelection()); + if (!mSelectionTracker.hasSelection()) { + Log.e(TAG, "Call to onItemClick w/o selection."); + if (DEBUG) throw new IllegalStateException("Call to onItemClick w/o selection."); + return; + } checkArgument(item != null); if (shouldExtendRange(e)) { diff --git a/app/src/main/java/androidx/recyclerview/selection/OperationMonitor.java b/app/src/main/java/androidx/recyclerview/selection/OperationMonitor.java index f5a4508d1a..53d4b82ed3 100644 --- a/app/src/main/java/androidx/recyclerview/selection/OperationMonitor.java +++ b/app/src/main/java/androidx/recyclerview/selection/OperationMonitor.java @@ -16,6 +16,7 @@ package androidx.recyclerview.selection; +import static androidx.annotation.RestrictTo.Scope.LIBRARY; import static androidx.core.util.Preconditions.checkArgument; import static androidx.core.util.Preconditions.checkState; import static androidx.recyclerview.selection.Shared.DEBUG; @@ -24,7 +25,7 @@ import android.util.Log; import androidx.annotation.MainThread; import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; +import androidx.annotation.RestrictTo; import java.util.ArrayList; import java.util.List; @@ -36,7 +37,7 @@ import java.util.List; * *

* The host {@link android.app.Activity} or {@link android.app.Fragment} should avoid changing - * {@link RecyclerView.Adapter Adapter} data while there + * {@link androidx.recyclerview.widget.RecyclerView.Adapter Adapter} data while there * are active selection operations, as this can result in a poor user experience. * *

@@ -46,17 +47,33 @@ public final class OperationMonitor { private static final String TAG = "OperationMonitor"; + private final List mListeners = new ArrayList<>(); + + // Ideally OperationMonitor would implement Resettable + // directly, but Metalava couldn't understand that + // `OperationMonitor` was public API while `Resettable` was + // not. This is our klunkuy workaround. + private final Resettable mResettable = new Resettable() { + + @Override + public boolean isResetRequired() { + return OperationMonitor.this.isResetRequired(); + } + + @Override + public void reset() { + OperationMonitor.this.reset(); + } + }; + private int mNumOps = 0; - private List mListeners = new ArrayList<>(); @MainThread synchronized void start() { mNumOps++; if (mNumOps == 1) { - for (OnChangeListener l : mListeners) { - l.onChanged(); - } + notifyStateChanged(); } if (DEBUG) Log.v(TAG, "Incremented content lock count to " + mNumOps + "."); @@ -64,29 +81,46 @@ public final class OperationMonitor { @MainThread synchronized void stop() { - checkState(mNumOps > 0); + if (mNumOps == 0) { + if (DEBUG) Log.w(TAG, "Stop called whith opt count of 0."); + return; + } mNumOps--; if (DEBUG) Log.v(TAG, "Decremented content lock count to " + mNumOps + "."); if (mNumOps == 0) { - for (OnChangeListener l : mListeners) { - l.onChanged(); - } + notifyStateChanged(); } } + /** @hide */ + @RestrictTo(LIBRARY) + @MainThread + synchronized void reset() { + if (DEBUG) Log.d(TAG, "Received reset request."); + if (mNumOps > 0) { + Log.w(TAG, "Resetting OperationMonitor with " + mNumOps + " active operations."); + } + mNumOps = 0; + notifyStateChanged(); + } + + /** @hide */ + @RestrictTo(LIBRARY) + synchronized boolean isResetRequired() { + return isStarted(); + } + /** * @return true if there are any running operations. */ - @SuppressWarnings("unused") public synchronized boolean isStarted() { return mNumOps > 0; } /** * Registers supplied listener to be notified when operation status changes. - * @param listener */ public void addListener(@NonNull OnChangeListener listener) { checkArgument(listener != null); @@ -95,7 +129,6 @@ public final class OperationMonitor { /** * Unregisters listener for further notifications. - * @param listener */ public void removeListener(@NonNull OnChangeListener listener) { checkArgument(listener != null); @@ -105,15 +138,27 @@ public final class OperationMonitor { /** * Allows other selection code to perform a precondition check asserting the state is locked. */ - void checkStarted() { - checkState(mNumOps > 0); + void checkStarted(boolean started) { + if (started) { + checkState(mNumOps > 0); + } else { + checkState(mNumOps == 0); + } + } + + private void notifyStateChanged() { + for (OnChangeListener l : mListeners) { + l.onChanged(); + } } /** - * Allows other selection code to perform a precondition check asserting the state is unlocked. + * Work around b/139109223. + * @hide */ - void checkStopped() { - checkState(mNumOps == 0); + @RestrictTo(LIBRARY) + @NonNull Resettable asResettable() { + return mResettable; } /** diff --git a/app/src/main/java/androidx/recyclerview/selection/PointerDragEventInterceptor.java b/app/src/main/java/androidx/recyclerview/selection/PointerDragEventInterceptor.java index 46ec5ddf10..ef0f077d69 100644 --- a/app/src/main/java/androidx/recyclerview/selection/PointerDragEventInterceptor.java +++ b/app/src/main/java/androidx/recyclerview/selection/PointerDragEventInterceptor.java @@ -25,19 +25,19 @@ import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener; /** - * OnItemTouchListener that delegates drag events to a drag listener, + * OnItemTouchListener that detects and delegates drag events to a drag listener, * else sends event to fallback {@link OnItemTouchListener}. * *

See {@link OnDragInitiatedListener} for details on implementing drag and drop. */ final class PointerDragEventInterceptor implements OnItemTouchListener { - private final ItemDetailsLookup mEventDetailsLookup; + private final ItemDetailsLookup mEventDetailsLookup; private final OnDragInitiatedListener mDragListener; - private @Nullable OnItemTouchListener mDelegate; + private OnItemTouchListener mDelegate; PointerDragEventInterceptor( - ItemDetailsLookup eventDetailsLookup, + ItemDetailsLookup eventDetailsLookup, OnDragInitiatedListener dragListener, @Nullable OnItemTouchListener delegate) { @@ -46,30 +46,29 @@ final class PointerDragEventInterceptor implements OnItemTouchListener { mEventDetailsLookup = eventDetailsLookup; mDragListener = dragListener; - mDelegate = delegate; + + if (delegate != null) { + mDelegate = delegate; + } else { + mDelegate = new DummyOnItemTouchListener(); + } } @Override public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { if (MotionEvents.isPointerDragEvent(e) && mEventDetailsLookup.inItemDragRegion(e)) { return mDragListener.onDragInitiated(e); - } else if (mDelegate != null) { - return mDelegate.onInterceptTouchEvent(rv, e); } - return false; + return mDelegate.onInterceptTouchEvent(rv, e); } @Override public void onTouchEvent(RecyclerView rv, MotionEvent e) { - if (mDelegate != null) { - mDelegate.onTouchEvent(rv, e); - } + mDelegate.onTouchEvent(rv, e); } @Override public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { - if (mDelegate != null) { - mDelegate.onRequestDisallowInterceptTouchEvent(disallowIntercept); - } + mDelegate.onRequestDisallowInterceptTouchEvent(disallowIntercept); } } diff --git a/app/src/main/java/androidx/recyclerview/selection/Range.java b/app/src/main/java/androidx/recyclerview/selection/Range.java index b11a14a9ad..433e0d2cb8 100644 --- a/app/src/main/java/androidx/recyclerview/selection/Range.java +++ b/app/src/main/java/androidx/recyclerview/selection/Range.java @@ -54,12 +54,14 @@ final class Range { * provisional selection will not affect the primary selection where the two may intersect. */ static final int TYPE_PROVISIONAL = 1; + @IntDef({ TYPE_PRIMARY, TYPE_PROVISIONAL }) @Retention(RetentionPolicy.SOURCE) - @interface RangeType {} + @interface RangeType { + } private static final String TAG = "Range"; @@ -69,9 +71,6 @@ final class Range { /** * Creates a new range anchored at {@code position}. - * - * @param position - * @param callbacks */ Range(int position, @NonNull Callbacks callbacks) { mBegin = position; @@ -118,7 +117,7 @@ final class Range { } else if (mEnd < mBegin) { reviseDescending(position, type); } - // the "else" case is covered by checkState at beginning of method. + // the "else" case is covered by checkArgument at beginning of method. mEnd = position; } @@ -170,11 +169,6 @@ final class Range { mCallbacks.updateForRange(begin, end, selected, type); } - boolean isOverlapping(int position, int count) { - return (position >= mBegin && position <= mEnd) || - (position + count >= mBegin && position + count <= mEnd); - } - @Override public String toString() { return "Range{begin=" + mBegin + ", end=" + mEnd + "}"; diff --git a/app/src/main/java/androidx/recyclerview/selection/ResetManager.java b/app/src/main/java/androidx/recyclerview/selection/ResetManager.java new file mode 100644 index 0000000000..254d013b63 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/selection/ResetManager.java @@ -0,0 +1,101 @@ +/* + * Copyright 2019 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 androidx.recyclerview.selection; + +import static androidx.recyclerview.selection.Shared.DEBUG; + +import android.util.Log; +import android.view.MotionEvent; + +import androidx.annotation.NonNull; +import androidx.recyclerview.selection.SelectionTracker.SelectionObserver; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener; + +import java.util.ArrayList; +import java.util.List; + +/** + * Class managing resetting of library state in response to specific + * events like clearing of selection and MotionEvent.ACTION_CANCEL + * events. + * + * @param Selection key type. @see {@link StorageStrategy} for supported types. + */ +final class ResetManager { + + private static final String TAG = "ResetManager"; + + private final List mResetHandlers = new ArrayList<>(); + + private final OnItemTouchListener mInputListener = new OnItemTouchListener() { + @Override + public boolean onInterceptTouchEvent(@NonNull RecyclerView unused, + @NonNull MotionEvent e) { + if (MotionEvents.isActionCancel(e)) { + if (DEBUG) Log.d(TAG, "Received CANCEL event."); + callResetHandlers(); + } + return false; + } + + @Override + public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { + } + + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + } + }; + + // Resettable interface has a #requiresReset method because DefaultSelectionTracker + // (owner of the state we observe with our SelectionObserver) is, itself, + // a Resettable. Such an arrangement introduces the real possibility of infinite recursion. + // When we call reset on DefaultSelectionTracker it'll eventually call back to + // notify us of the change via onSelectionCleared. We avoid recursion by + // checking #requiresReset before calling reset again. + private final SelectionObserver mSelectionObserver = new SelectionObserver() { + @Override + protected void onSelectionCleared() { + if (DEBUG) Log.d(TAG, "Received onSelectionCleared event."); + callResetHandlers(); + } + }; + + SelectionObserver getSelectionObserver() { + return mSelectionObserver; + } + + OnItemTouchListener getInputListener() { + return mInputListener; + } + + /** + * Registers a new Resettable. + */ + void addResetHandler(@NonNull Resettable handler) { + mResetHandlers.add(handler); + } + + void callResetHandlers() { + for (Resettable handler : mResetHandlers) { + if (handler.isResetRequired()) { + handler.reset(); + } + } + } +} diff --git a/app/src/main/java/androidx/recyclerview/selection/Resettable.java b/app/src/main/java/androidx/recyclerview/selection/Resettable.java new file mode 100644 index 0000000000..85092aa215 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/selection/Resettable.java @@ -0,0 +1,45 @@ +/* + * Copyright 2019 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 androidx.recyclerview.selection; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY; + +import androidx.annotation.RestrictTo; + +/** + * Represents an object that can be reset and can advise on it's + * need to be reset. + * + *

Calling {@link #isResetRequired()} on an instance of {@link Resettable} + * should always return false when called immediately after {@link #reset()} + * has been called. + * + * @hide + */ +@RestrictTo(LIBRARY) +public interface Resettable { + + /** + * @return true if the object requires reset. + */ + boolean isResetRequired(); + + /** + * Resets the object state. + */ + void reset(); +} diff --git a/app/src/main/java/androidx/recyclerview/selection/Selection.java b/app/src/main/java/androidx/recyclerview/selection/Selection.java index 155789d91f..11dc284bab 100644 --- a/app/src/main/java/androidx/recyclerview/selection/Selection.java +++ b/app/src/main/java/androidx/recyclerview/selection/Selection.java @@ -54,9 +54,8 @@ import java.util.Set; * (which can be initiated by long pressing an unselected item while there is an * existing selection). * - * @see MutableSelection - * * @param Selection key type. @see {@link StorageStrategy} for supported types. + * @see MutableSelection */ public class Selection implements Iterable { @@ -78,7 +77,6 @@ public class Selection implements Iterable { } /** - * @param key * @return true if the position is currently selected. */ public boolean contains(@Nullable K key) { @@ -92,7 +90,7 @@ public class Selection implements Iterable { * {@inheritDoc} */ @Override - public Iterator iterator() { + public @NonNull Iterator iterator() { return mSelection.iterator(); } @@ -114,12 +112,13 @@ public class Selection implements Iterable { * Sets the provisional selection, which is a temporary selection that can be saved, * canceled, or adjusted at a later time. When a new provision selection is applied, the old * one (if it exists) is abandoned. + * * @return Map of ids added or removed. Added ids have a value of true, removed are false. */ Map setProvisionalSelection(@NonNull Set newSelection) { Map delta = new LinkedHashMap<>(); - for (K key: mProvisionalSelection) { + for (K key : mProvisionalSelection) { // Mark each item that used to be in the provisional selection // but is not in the new provisional selection. if (!newSelection.contains(key) && !mSelection.contains(key)) { @@ -127,7 +126,7 @@ public class Selection implements Iterable { } } - for (K key: mSelection) { + for (K key : mSelection) { // Mark each item that in the selection but is not in the new // provisional selection. if (!newSelection.contains(key)) { @@ -135,7 +134,7 @@ public class Selection implements Iterable { } } - for (K key: newSelection) { + for (K key : newSelection) { // Mark each item that was not previously in the selection but is in the new // provisional selection. if (!mSelection.contains(key) && !mProvisionalSelection.contains(key)) { @@ -146,7 +145,7 @@ public class Selection implements Iterable { // Now, iterate through the changes and actually add/remove them to/from the current // selection. This could not be done in the previous loops because changing the size of // the selection mid-iteration changes iteration order erroneously. - for (Map.Entry entry: delta.entrySet()) { + for (Map.Entry entry : delta.entrySet()) { K key = entry.getKey(); if (entry.getValue()) { mProvisionalSelection.add(key); @@ -221,11 +220,11 @@ public class Selection implements Iterable { StringBuilder buffer = new StringBuilder(size() * 28); buffer.append("Selection{") - .append("primary{size=" + mSelection.size()) - .append(", entries=" + mSelection) - .append("}, provisional{size=" + mProvisionalSelection.size()) - .append(", entries=" + mProvisionalSelection) - .append("}}"); + .append("primary{size=" + mSelection.size()) + .append(", entries=" + mSelection) + .append("}, provisional{size=" + mProvisionalSelection.size()) + .append(", entries=" + mProvisionalSelection) + .append("}}"); return buffer.toString(); } @@ -237,10 +236,10 @@ public class Selection implements Iterable { @Override public boolean equals(Object other) { return (this == other) - || (other instanceof Selection && isEqualTo((Selection) other)); + || (other instanceof Selection && isEqualTo((Selection) other)); } - private boolean isEqualTo(Selection other) { + private boolean isEqualTo(Selection other) { return mSelection.equals(other.mSelection) && mProvisionalSelection.equals(other.mProvisionalSelection); } diff --git a/app/src/main/java/androidx/recyclerview/selection/SelectionPredicates.java b/app/src/main/java/androidx/recyclerview/selection/SelectionPredicates.java index 1e13bdefb3..58780affca 100644 --- a/app/src/main/java/androidx/recyclerview/selection/SelectionPredicates.java +++ b/app/src/main/java/androidx/recyclerview/selection/SelectionPredicates.java @@ -34,7 +34,7 @@ public final class SelectionPredicates { * @param Selection key type. @see {@link StorageStrategy} for supported types. * @return */ - public static SelectionPredicate createSelectAnything() { + public static @NonNull SelectionPredicate createSelectAnything() { return new SelectionPredicate() { @Override public boolean canSetStateForKey(@NonNull K key, boolean nextState) { @@ -60,7 +60,7 @@ public final class SelectionPredicates { * @param Selection key type. @see {@link StorageStrategy} for supported types. * @return */ - public static SelectionPredicate createSelectSingleAnything() { + public static @NonNull SelectionPredicate createSelectSingleAnything() { return new SelectionPredicate() { @Override public boolean canSetStateForKey(@NonNull K key, boolean nextState) { diff --git a/app/src/main/java/androidx/recyclerview/selection/SelectionTracker.java b/app/src/main/java/androidx/recyclerview/selection/SelectionTracker.java index 032fa35316..910935a0ac 100644 --- a/app/src/main/java/androidx/recyclerview/selection/SelectionTracker.java +++ b/app/src/main/java/androidx/recyclerview/selection/SelectionTracker.java @@ -22,6 +22,7 @@ import static androidx.core.util.Preconditions.checkArgument; import android.content.Context; import android.os.Bundle; import android.os.Parcelable; +import android.util.Log; import android.view.GestureDetector; import android.view.HapticFeedbackConstants; import android.view.MotionEvent; @@ -89,12 +90,14 @@ import java.util.Set; */ public abstract class SelectionTracker { + private static final String TAG = "SelectionTracker"; + /** * This value is included in the payload when SelectionTracker notifies RecyclerView * of changes to selection. Look for this value in the {@code payload} * Object argument supplied to * {@link RecyclerView.Adapter#onBindViewHolder - * Adapter#onBindViewHolder}. + * Adapter#onBindViewHolder}. * If present the call is occurring in response to a selection state change. * This would be a good opportunity to animate changes between unselected and selected state. * When state is being restored, this argument will not be present. @@ -110,7 +113,7 @@ public abstract class SelectionTracker { * may use an observer to control the enabled status of menu items, * or to initiate {@link android.view.ActionMode}. */ - public abstract void addObserver(SelectionObserver observer); + public abstract void addObserver(@NonNull SelectionObserver observer); /** @return true if has a selection */ public abstract boolean hasSelection(); @@ -123,7 +126,7 @@ public abstract class SelectionTracker { * of the selection that will not reflect future changes * to selection. */ - public abstract Selection getSelection(); + public abstract @NonNull Selection getSelection(); /** * Updates {@code dest} to reflect the current selection. @@ -144,9 +147,8 @@ public abstract class SelectionTracker { * This affords clients the ability to restore selection from selection saved * in Activity state. * - * @see StorageStrategy details on selection state support. - * * @param selection selection being restored. + * @see StorageStrategy details on selection state support. */ protected abstract void restoreSelection(@NonNull Selection selection); @@ -181,7 +183,7 @@ public abstract class SelectionTracker { /** @hide */ @RestrictTo(LIBRARY) - protected abstract AdapterDataObserver getAdapterDataObserver(); + protected abstract @NonNull AdapterDataObserver getAdapterDataObserver(); /** * Attempts to establish a range selection at {@code position}, selecting the item @@ -203,9 +205,9 @@ public abstract class SelectionTracker { * (see {@link #isRangeActive()}. Items in the range [anchor, end] will be * selected after consulting SelectionPredicate. * - * @param position The new end position for the selection range. + * @param position The new end position for the selection range. * @throws IllegalStateException if a range selection is not active. Range selection - * must have been started by a call to {@link #startRange(int)}. + * must have been started by a call to {@link #startRange(int)}. * @hide */ @RestrictTo(LIBRARY) @@ -215,6 +217,7 @@ public abstract class SelectionTracker { * Clears an in-progress range selection. Provisional range selection established * using {@link #extendProvisionalRange(int)} will be cleared (unless * {@link #mergeProvisionalSelection()} is called first.) + * * @hide */ @RestrictTo(LIBRARY) @@ -251,7 +254,7 @@ public abstract class SelectionTracker { /** * Sets the provisional selection, replacing any existing selection. - * @param newSelection + * * @hide */ @RestrictTo(LIBRARY) @@ -259,6 +262,7 @@ public abstract class SelectionTracker { /** * Clears any existing provisional selection + * * @hide */ @RestrictTo(LIBRARY) @@ -267,6 +271,7 @@ public abstract class SelectionTracker { /** * Converts the provisional selection into primary selection, then clears * provisional selection. + * * @hide */ @RestrictTo(LIBRARY) @@ -300,6 +305,16 @@ public abstract class SelectionTracker { public void onItemStateChanged(@NonNull K key, boolean selected) { } + /** + * Called when Selection is cleared. + * TODO(smckay): Make public in a future public API. + * + * @hide + */ + @RestrictTo(LIBRARY) + protected void onSelectionCleared() { + } + /** * Called when the underlying data set has changed. After this method is called * SelectionTracker will traverse the existing selection, @@ -338,7 +353,7 @@ public abstract class SelectionTracker { /** * Validates a change to selection for a specific key. * - * @param key the item key + * @param key the item key * @param nextState the next potential selected/unselected state * @return true if the item at {@code id} can be set to {@code nextState}. */ @@ -348,7 +363,7 @@ public abstract class SelectionTracker { * Validates a change to selection for a specific position. If necessary * use {@link ItemKeyProvider} to identy associated key. * - * @param position the item position + * @param position the item position * @param nextState the next potential selected/unselected state * @return true if the item at {@code id} can be set to {@code nextState}. */ @@ -384,7 +399,7 @@ public abstract class SelectionTracker { * new MyDetailsLookup(recyclerView), * StorageStrategy.createParcelableStorage(Uri.class)) * .build(); - * + * * *

* Restricting which items can be selected and limiting selection size @@ -399,14 +414,14 @@ public abstract class SelectionTracker { * by supplying {@link SelectionPredicates#createSelectSingleAnything()}. * * SelectionTracker tracker = new SelectionTracker.Builder<>( - * "my-string-selection", - * recyclerView, - * new DemoStableIdProvider(recyclerView.getAdapter()), - * new MyDetailsLookup(recyclerView), - * StorageStrategy.createStringStorage()) - * .withSelectionPredicate(SelectionPredicates#createSelectSingleAnything()) - * .build(); - * + * "my-string-selection", + * recyclerView, + * new DemoStableIdProvider(recyclerView.getAdapter()), + * new MyDetailsLookup(recyclerView), + * StorageStrategy.createStringStorage()) + * .withSelectionPredicate(SelectionPredicates#createSelectSingleAnything()) + * .build(); + * *

* Retaining state across Android lifecycle events * @@ -447,25 +462,25 @@ public abstract class SelectionTracker { * private SelectionTracker mTracker; * * public void onCreate(Bundle savedInstanceState) { - * // See above for details on constructing a SelectionTracker instance. + * // See above for details on constructing a SelectionTracker instance. * - * if (savedInstanceState != null) { - * mTracker.onRestoreInstanceState(savedInstanceState); - * } + * if (savedInstanceState != null) { + * mTracker.onRestoreInstanceState(savedInstanceState); + * } * } * * protected void onSaveInstanceState(Bundle outState) { - * super.onSaveInstanceState(outState); - * mTracker.onSaveInstanceState(outState); + * super.onSaveInstanceState(outState); + * mTracker.onSaveInstanceState(outState); * } * * * @param Selection key type. Built in support is provided for {@link String}, - * {@link Long}, and {@link Parcelable}. {@link StorageStrategy} - * provides factory methods for each type: - * {@link StorageStrategy#createStringStorage()}, - * {@link StorageStrategy#createParcelableStorage(Class)}, - * {@link StorageStrategy#createLongStorage()} + * {@link Long}, and {@link Parcelable}. {@link StorageStrategy} + * provides factory methods for each type: + * {@link StorageStrategy#createStringStorage()}, + * {@link StorageStrategy#createParcelableStorage(Class)}, + * {@link StorageStrategy#createLongStorage()} */ public static final class Builder { @@ -490,12 +505,12 @@ public abstract class SelectionTracker { private BandPredicate mBandPredicate; private int mBandOverlayId = eu.faircode.email.R.drawable.selection_band_overlay; - private int[] mGestureToolTypes = new int[] { - MotionEvent.TOOL_TYPE_FINGER, - MotionEvent.TOOL_TYPE_UNKNOWN + // TODO(b/144500333): Remove support for overriding gesture and pointer tooltypes. + private int[] mGestureToolTypes = new int[]{ + MotionEvent.TOOL_TYPE_FINGER }; - private int[] mPointerToolTypes = new int[] { + private int[] mPointerToolTypes = new int[]{ MotionEvent.TOOL_TYPE_MOUSE }; @@ -503,13 +518,13 @@ public abstract class SelectionTracker { * Creates a new SelectionTracker.Builder useful for configuring and creating * a new SelectionTracker for use with your {@link RecyclerView}. * - * @param selectionId A unique string identifying this selection in the context - * of the activity or fragment. - * @param recyclerView the owning RecyclerView - * @param keyProvider the source of selection keys + * @param selectionId A unique string identifying this selection in the context + * of the activity or fragment. + * @param recyclerView the owning RecyclerView + * @param keyProvider the source of selection keys * @param detailsLookup the source of information about RecyclerView items. - * @param storage Strategy for type-safe storage of selection state in - * {@link Bundle}. + * @param storage Strategy for type-safe storage of selection state in + * {@link Bundle}. */ public Builder( @NonNull String selectionId, @@ -545,7 +560,7 @@ public abstract class SelectionTracker { * @param predicate the predicate to be used. * @return this */ - public Builder withSelectionPredicate( + public @NonNull Builder withSelectionPredicate( @NonNull SelectionPredicate predicate) { checkArgument(predicate != null); @@ -560,7 +575,7 @@ public abstract class SelectionTracker { * @param monitor the monitor to be used * @return this */ - public Builder withOperationMonitor( + public @NonNull Builder withOperationMonitor( @NonNull OperationMonitor monitor) { checkArgument(monitor != null); @@ -574,7 +589,7 @@ public abstract class SelectionTracker { * @param delegate the delegate to be used * @return this */ - public Builder withFocusDelegate(@NonNull FocusDelegate delegate) { + public @NonNull Builder withFocusDelegate(@NonNull FocusDelegate delegate) { checkArgument(delegate != null); mFocusDelegate = delegate; return this; @@ -586,7 +601,7 @@ public abstract class SelectionTracker { * @param listener the listener to be used * @return this */ - public Builder withOnItemActivatedListener( + public @NonNull Builder withOnItemActivatedListener( @NonNull OnItemActivatedListener listener) { checkArgument(listener != null); @@ -601,7 +616,7 @@ public abstract class SelectionTracker { * @param listener the listener to be used * @return this */ - public Builder withOnContextClickListener( + public @NonNull Builder withOnContextClickListener( @NonNull OnContextClickListener listener) { checkArgument(listener != null); @@ -616,7 +631,7 @@ public abstract class SelectionTracker { * @param listener the listener to be used * @return this */ - public Builder withOnDragInitiatedListener( + public @NonNull Builder withOnDragInitiatedListener( @NonNull OnDragInitiatedListener listener) { checkArgument(listener != null); @@ -627,12 +642,17 @@ public abstract class SelectionTracker { /** * Replaces default tap and gesture tool-types. Defaults are: - * {@link MotionEvent#TOOL_TYPE_FINGER} and {@link MotionEvent#TOOL_TYPE_UNKNOWN}. + * {@link MotionEvent#TOOL_TYPE_FINGER}. * * @param toolTypes the tool types to be used * @return this + * + * @deprecated GestureSelection is best bound to {@link MotionEvent#TOOL_TYPE_FINGER}, + * and only that tool type. This method will be removed in a future release. */ - public Builder withGestureTooltypes(int... toolTypes) { + @Deprecated + public @NonNull Builder withGestureTooltypes(@NonNull int... toolTypes) { + Log.w(TAG, "Setting gestureTooltypes is likely to result in unexpected behavior."); mGestureToolTypes = toolTypes; return this; } @@ -640,22 +660,19 @@ public abstract class SelectionTracker { /** * Replaces default band overlay. * - * @param bandOverlayId * @return this */ - public Builder withBandOverlay(@DrawableRes int bandOverlayId) { + public @NonNull Builder withBandOverlay(@DrawableRes int bandOverlayId) { mBandOverlayId = bandOverlayId; return this; } /** * Replaces default band predicate. - * @param bandPredicate + * * @return this */ - public Builder withBandPredicate(@NonNull BandPredicate bandPredicate) { - checkArgument(bandPredicate != null); - + public @NonNull Builder withBandPredicate(@NonNull BandPredicate bandPredicate) { mBandPredicate = bandPredicate; return this; } @@ -668,8 +685,13 @@ public abstract class SelectionTracker { * * @param toolTypes the tool types to be used * @return this + * + * @deprecated PointerSelection is best bound to {@link MotionEvent#TOOL_TYPE_MOUSE}, + * and only that tool type. This method will be removed in a future release. */ - public Builder withPointerTooltypes(int... toolTypes) { + @Deprecated + public @NonNull Builder withPointerTooltypes(@NonNull int... toolTypes) { + Log.w(TAG, "Setting pointerTooltypes is likely to result in unexpected behavior."); mPointerToolTypes = toolTypes; return this; } @@ -679,9 +701,9 @@ public abstract class SelectionTracker { * * @return this */ - public SelectionTracker build() { + public @NonNull SelectionTracker build() { - SelectionTracker tracker = new DefaultSelectionTracker<>( + DefaultSelectionTracker tracker = new DefaultSelectionTracker<>( mSelectionId, mKeyProvider, mSelectionPredicate, mStorage); // Event glue between RecyclerView and SelectionTracker keeps the classes separate @@ -689,6 +711,8 @@ public abstract class SelectionTracker { // represent the same data in different ways. EventBridge.install(mAdapter, tracker, mKeyProvider); + // Scroller is stateful and can be reset, but we don't manage it directly. + // GestureSelectionHelper will reset scroller when it is reset. AutoScroller scroller = new ViewAutoScroller(ViewAutoScroller.createScrollHost(mRecyclerView)); @@ -698,14 +722,10 @@ public abstract class SelectionTracker { // GestureRouter is responsible for routing GestureDetector events // to tool-type specific handlers. - GestureRouter gestureRouter = new GestureRouter<>(); - GestureDetector gestureDetector = new GestureDetector(mContext, gestureRouter); + GestureRouter> gestureRouter = new GestureRouter<>(); - // TouchEventRouter takes its name from RecyclerView#OnItemTouchListener. - // Despite "Touch" being in the name, it receives events for all types of tools. - // This class is responsible for routing events to tool-type specific handlers, - // and if not handled by a handler, on to a GestureDetector for analysis. - TouchEventRouter eventRouter = new TouchEventRouter(gestureDetector); + // GestureDetector cancels itself in response to ACTION_CANCEL events. + GestureDetector gestureDetector = new GestureDetector(mContext, gestureRouter); // GestureSelectionHelper provides logic that interprets a combination // of motions and gestures in order to provide gesture driven selection support @@ -713,8 +733,37 @@ public abstract class SelectionTracker { final GestureSelectionHelper gestureHelper = GestureSelectionHelper.create( tracker, mSelectionPredicate, mRecyclerView, scroller, mMonitor); - // Finally hook the framework up to listening to recycle view events. + // EventRouter receives events for RecyclerView, dispatching to handlers + // registered by tool-type. + EventRouter eventRouter = new EventRouter(); + + // Finally hook the framework up to listening to RecycleView events. mRecyclerView.addOnItemTouchListener(eventRouter); + mRecyclerView.addOnItemTouchListener( + new GestureDetectorOnItemTouchListenerAdapter(gestureDetector)); + + // Reset manager listens for cancel events from RecyclerView. In response to that it + // advises other classes it is time to reset state. + ResetManager resetMgr = new ResetManager<>(); + + // Register ResetManager to: + // + // 1. Monitor selection reset which can be invoked by clients in response + // to back key press and some application lifecycle events. + // + // 2. Monitor ACTION_CANCEL events (which arrive exclusively + // via TOOL_TYPE_UNKNOWN). + tracker.addObserver(resetMgr.getSelectionObserver()); + + // CAUTION! Registering resetMgr directly with RecyclerView#addOnItemTouchListener + // will not work as expected. Once EventRouter returns true, RecyclerView will + // no longer dispatch any events to other listeners for the duration of the + // stream, not even ACTION_CANCEL events. + eventRouter.set(MotionEvent.TOOL_TYPE_UNKNOWN, resetMgr.getInputListener()); + + resetMgr.addResetHandler(tracker); + resetMgr.addResetHandler(mMonitor.asResettable()); + resetMgr.addResetHandler(gestureHelper); // But before you move on, there's more work to do. Event plumbing has been // installed, but we haven't registered any of our helpers or callbacks. @@ -757,7 +806,7 @@ public abstract class SelectionTracker { // Provides high level glue for binding touch events // and gestures to selection framework. - TouchInputHandler touchHandler = new TouchInputHandler( + TouchInputHandler touchHandler = new TouchInputHandler<>( tracker, mKeyProvider, mDetailsLookup, @@ -768,8 +817,8 @@ public abstract class SelectionTracker { if (mSelectionPredicate.canSelectMultiple()) { try { gestureHelper.start(); - } catch (IllegalStateException ex) { - eu.faircode.email.Log.w(ex); + } catch (Throwable ex) { + eu.faircode.email.Log.e(ex); } } } @@ -786,7 +835,7 @@ public abstract class SelectionTracker { for (int toolType : mGestureToolTypes) { gestureRouter.register(toolType, touchHandler); - eventRouter.register(toolType, gestureHelper); + eventRouter.set(toolType, gestureHelper); } // Provides high level glue for binding mouse events and gestures @@ -803,7 +852,7 @@ public abstract class SelectionTracker { gestureRouter.register(toolType, mouseHandler); } - @Nullable BandSelectionHelper bandHelper = null; + @Nullable BandSelectionHelper bandHelper = null; // Band selection not supported in single select mode, or when key access // is limited to anything less than the entire corpus. @@ -824,14 +873,14 @@ public abstract class SelectionTracker { mBandPredicate, mFocusDelegate, mMonitor); + + resetMgr.addResetHandler(bandHelper); } OnItemTouchListener pointerEventHandler = new PointerDragEventInterceptor( mDetailsLookup, mOnDragInitiatedListener, bandHelper); - for (int toolType : mPointerToolTypes) { - eventRouter.register(toolType, pointerEventHandler); - } + eventRouter.set(MotionEvent.TOOL_TYPE_MOUSE, pointerEventHandler); return tracker; } diff --git a/app/src/main/java/androidx/recyclerview/selection/StorageStrategy.java b/app/src/main/java/androidx/recyclerview/selection/StorageStrategy.java index a27752f159..1fb7b7504b 100644 --- a/app/src/main/java/androidx/recyclerview/selection/StorageStrategy.java +++ b/app/src/main/java/androidx/recyclerview/selection/StorageStrategy.java @@ -39,11 +39,11 @@ import java.util.ArrayList; * for more detailed advice on which key type to use for your selection keys. * * @param Selection key type. Built in support is provided for String, Long, and Parcelable - * types. Use the respective factory method to create a StorageStrategy instance - * appropriate to the desired type. - * {@link #createStringStorage()}, - * {@link #createParcelableStorage(Class)}, - * {@link #createLongStorage()} + * types. Use the respective factory method to create a StorageStrategy instance + * appropriate to the desired type. + * {@link #createStringStorage()}, + * {@link #createParcelableStorage(Class)}, + * {@link #createLongStorage()} */ public abstract class StorageStrategy { @@ -69,7 +69,6 @@ public abstract class StorageStrategy { * Create a {@link Selection} from supplied {@link Bundle}. * * @param state Bundle instance that may contain parceled Selection instance. - * @return */ public abstract @Nullable Selection asSelection(@NonNull Bundle state); @@ -77,7 +76,6 @@ public abstract class StorageStrategy { * Creates a {@link Bundle} from supplied {@link Selection}. * * @param selection The selection to asBundle. - * @return */ public abstract @NonNull Bundle asBundle(@NonNull Selection selection); @@ -89,21 +87,22 @@ public abstract class StorageStrategy { * @return StorageStrategy suitable for use with {@link Parcelable} keys * (like {@link android.net.Uri}). */ - public static StorageStrategy createParcelableStorage(Class type) { - return new ParcelableStorageStrategy(type); + public static @NonNull StorageStrategy createParcelableStorage( + @NonNull Class type) { + return new ParcelableStorageStrategy<>(type); } /** * @return StorageStrategy suitable for use with {@link String} keys. */ - public static StorageStrategy createStringStorage() { + public static @NonNull StorageStrategy createStringStorage() { return new StringStorageStrategy(); } /** * @return StorageStrategy suitable for use with {@link Long} keys. */ - public static StorageStrategy createLongStorage() { + public static @NonNull StorageStrategy createLongStorage() { return new LongStorageStrategy(); } @@ -191,7 +190,7 @@ public abstract class StorageStrategy { private static class ParcelableStorageStrategy extends StorageStrategy { - ParcelableStorageStrategy(Class type) { + ParcelableStorageStrategy(@NonNull Class type) { super(type); checkArgument(Parcelable.class.isAssignableFrom(type)); } diff --git a/app/src/main/java/androidx/recyclerview/selection/ToolHandlerRegistry.java b/app/src/main/java/androidx/recyclerview/selection/ToolHandlerRegistry.java index 8fc82c6297..856ebb3bc8 100644 --- a/app/src/main/java/androidx/recyclerview/selection/ToolHandlerRegistry.java +++ b/app/src/main/java/androidx/recyclerview/selection/ToolHandlerRegistry.java @@ -30,29 +30,27 @@ import java.util.List; /** * Registry for tool specific event handler. This provides map like functionality, * along with fallback to a default handler, while avoiding auto-boxing of tool - * type values that would be necessitated where a Map used. + * type values that would be necessitated were a Map used.k + * + *

ToolHandlerRegistry guarantees that it will never return a null handler ensuring + * client code isn't peppered with null checks. To that end a default handler + * is required. This default handler will be returned when a handler matching + * the event tooltype has not be registered using {@link #set(int, T)}. * * @param type of item being registered. */ final class ToolHandlerRegistry { - // Currently there are four known input types. ERASER is the last one, so has the - // highest value. UNKNOWN is zero, so we add one. This allows delegates to be - // registered by type, and avoid the auto-boxing that would be necessary were we - // to store delegates in a Map. - private static final int NUM_INPUT_TYPES = MotionEvent.TOOL_TYPE_ERASER + 1; - + // list with one null entry for each known tooltype (0-4). + // See MotionEvent.TOOL_TYPE_ERASER for details. We're using a list here because + // it is parameterized type friendly, and a natural container given that + // the index values are 0-based ints. private final List mHandlers = Arrays.asList(null, null, null, null, null); private final T mDefault; ToolHandlerRegistry(@NonNull T defaultDelegate) { checkArgument(defaultDelegate != null); mDefault = defaultDelegate; - - // Initialize all values to null. - for (int i = 0; i < NUM_INPUT_TYPES; i++) { - mHandlers.set(i, null); - } } /** diff --git a/app/src/main/java/androidx/recyclerview/selection/TouchEventRouter.java b/app/src/main/java/androidx/recyclerview/selection/TouchEventRouter.java deleted file mode 100644 index a5501da1d1..0000000000 --- a/app/src/main/java/androidx/recyclerview/selection/TouchEventRouter.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright 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 androidx.recyclerview.selection; - -import static androidx.core.util.Preconditions.checkArgument; - -import android.view.GestureDetector; -import android.view.MotionEvent; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; -import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener; - -/** - * A class responsible for routing MotionEvents to tool-type specific handlers, - * and if not handled by a handler, on to a {@link GestureDetector} for further - * processing. - * - *

- * TouchEventRouter takes its name from - * {@link RecyclerView#addOnItemTouchListener(OnItemTouchListener)}. Despite "Touch" - * being in the name, it receives MotionEvents for all types of tools. - */ -final class TouchEventRouter implements OnItemTouchListener { - - private static final String TAG = "TouchEventRouter"; - - private final GestureDetector mDetector; - private final ToolHandlerRegistry mDelegates; - - TouchEventRouter( - @NonNull GestureDetector detector, @NonNull OnItemTouchListener defaultDelegate) { - - checkArgument(detector != null); - checkArgument(defaultDelegate != null); - - mDetector = detector; - mDelegates = new ToolHandlerRegistry<>(defaultDelegate); - } - - TouchEventRouter(@NonNull GestureDetector detector) { - this( - detector, - // Supply a fallback listener does nothing...because the caller - // didn't supply a fallback. - new OnItemTouchListener() { - @Override - public boolean onInterceptTouchEvent( - @NonNull RecyclerView unused, @NonNull MotionEvent e) { - - return false; - } - - @Override - public void onTouchEvent( - @NonNull RecyclerView unused, @NonNull MotionEvent e) { - } - - @Override - public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { - } - }); - } - - /** - * @param toolType See MotionEvent for details on available types. - * @param delegate An {@link OnItemTouchListener} to receive events - * of {@code toolType}. - */ - void register(int toolType, @NonNull OnItemTouchListener delegate) { - checkArgument(delegate != null); - mDelegates.set(toolType, delegate); - } - - @Override - public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { - boolean handled = mDelegates.get(e).onInterceptTouchEvent(rv, e); - - // Forward all events to UserInputHandler. - // This is necessary since UserInputHandler needs to always see the first DOWN event. Or - // else all future UP events will be tossed. - handled |= mDetector.onTouchEvent(e); - - return handled; - } - - @Override - public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { - mDelegates.get(e).onTouchEvent(rv, e); - - // Note: even though this event is being handled as part of gestures such as drag and band, - // continue forwarding to the GestureDetector. The detector needs to see the entire cluster - // of events in order to properly interpret other gestures, such as long press. - mDetector.onTouchEvent(e); - } - - @Override - public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {} -} diff --git a/app/src/main/java/androidx/recyclerview/selection/TouchInputHandler.java b/app/src/main/java/androidx/recyclerview/selection/TouchInputHandler.java index 48db78ec8d..c434ced07f 100644 --- a/app/src/main/java/androidx/recyclerview/selection/TouchInputHandler.java +++ b/app/src/main/java/androidx/recyclerview/selection/TouchInputHandler.java @@ -17,6 +17,7 @@ package androidx.recyclerview.selection; import static androidx.core.util.Preconditions.checkArgument; +import static androidx.recyclerview.selection.Shared.DEBUG; import android.util.Log; import android.view.MotionEvent; @@ -36,7 +37,6 @@ import androidx.recyclerview.widget.RecyclerView; final class TouchInputHandler extends MotionInputHandler { private static final String TAG = "TouchInputDelegate"; - private static final boolean DEBUG = false; private final ItemDetailsLookup mDetailsLookup; private final SelectionPredicate mSelectionPredicate; @@ -75,6 +75,11 @@ final class TouchInputHandler extends MotionInputHandler { @Override public boolean onSingleTapUp(@NonNull MotionEvent e) { + if (DEBUG) { + checkArgument(MotionEvents.isFingerEvent(e)); + checkArgument(MotionEvents.isActionUp(e)); + } + if (!mDetailsLookup.overItemWithSelectionKey(e)) { if (DEBUG) Log.d(TAG, "Tap not associated w/ model item. Clearing selection."); mSelectionTracker.clearSelection(); @@ -113,6 +118,11 @@ final class TouchInputHandler extends MotionInputHandler { @Override public void onLongPress(@NonNull MotionEvent e) { + if (DEBUG) { + checkArgument(MotionEvents.isFingerEvent(e)); + checkArgument(MotionEvents.isActionDown(e)); + } + if (!mDetailsLookup.overItemWithSelectionKey(e)) { if (DEBUG) Log.d(TAG, "Ignoring LongPress on non-model-backed item."); return; @@ -124,8 +134,6 @@ final class TouchInputHandler extends MotionInputHandler { return; } - boolean handled = false; - if (shouldExtendRange(e)) { extendSelectionRange(item); mHapticPerformer.run(); diff --git a/app/src/main/java/androidx/recyclerview/selection/package-info.java b/app/src/main/java/androidx/recyclerview/selection/package-info.java index f344ba227c..e388a527ad 100644 --- a/app/src/main/java/androidx/recyclerview/selection/package-info.java +++ b/app/src/main/java/androidx/recyclerview/selection/package-info.java @@ -97,7 +97,7 @@ * * *

- * Example usage (with {@code Long} selection keys: + * Example usage (with {@code Long} selection keys): *

SelectionTracker tracker = new SelectionTracker.Builder<>(
  *        "my-selection-id",
  *        recyclerView,
diff --git a/patches/recyclerview-selection.patch b/patches/recyclerview-selection.patch
index 3afe27c217..04aaebe43b 100644
--- a/patches/recyclerview-selection.patch
+++ b/patches/recyclerview-selection.patch
@@ -1,34 +1,34 @@
 diff --git a/app/src/main/java/androidx/recyclerview/selection/SelectionTracker.java b/app/src/main/java/androidx/recyclerview/selection/SelectionTracker.java
-index 76087ea4..121fbd14 100644
+index 50cc4ceb7..910935a0a 100644
 --- a/app/src/main/java/androidx/recyclerview/selection/SelectionTracker.java
 +++ b/app/src/main/java/androidx/recyclerview/selection/SelectionTracker.java
-@@ -488,7 +488,7 @@ public abstract class SelectionTracker {
+@@ -503,7 +503,7 @@ public abstract class SelectionTracker {
          private OnContextClickListener mOnContextClickListener;
  
          private BandPredicate mBandPredicate;
 -        private int mBandOverlayId = R.drawable.selection_band_overlay;
 +        private int mBandOverlayId = eu.faircode.email.R.drawable.selection_band_overlay;
  
-         private int[] mGestureToolTypes = new int[] {
-                 MotionEvent.TOOL_TYPE_FINGER,
-@@ -766,7 +766,11 @@ public abstract class SelectionTracker {
+         // TODO(b/144500333): Remove support for overriding gesture and pointer tooltypes.
+         private int[] mGestureToolTypes = new int[]{
+@@ -815,7 +815,11 @@ public abstract class SelectionTracker {
                          @Override
                          public void run() {
                              if (mSelectionPredicate.canSelectMultiple()) {
 -                                gestureHelper.start();
 +                                try {
 +                                    gestureHelper.start();
-+                                } catch (IllegalStateException ex) {
-+                                    ex.printStackTrace();
++                                } catch (Throwable ex) {
++                                    eu.faircode.email.Log.e(ex);
 +                                }
                              }
                          }
                      },
 diff --git a/app/src/main/java/androidx/recyclerview/selection/TouchInputHandler.java b/app/src/main/java/androidx/recyclerview/selection/TouchInputHandler.java
-index d82812cc..48db78ec 100644
+index be4c3fa0b..c434ced07 100644
 --- a/app/src/main/java/androidx/recyclerview/selection/TouchInputHandler.java
 +++ b/app/src/main/java/androidx/recyclerview/selection/TouchInputHandler.java
-@@ -107,6 +107,11 @@ final class TouchInputHandler extends MotionInputHandler {
+@@ -112,6 +112,11 @@ final class TouchInputHandler extends MotionInputHandler {
      }
  
      @Override
@@ -38,5 +38,5 @@ index d82812cc..48db78ec 100644
 +
 +    @Override
      public void onLongPress(@NonNull MotionEvent e) {
-         if (!mDetailsLookup.overItemWithSelectionKey(e)) {
-             if (DEBUG) Log.d(TAG, "Ignoring LongPress on non-model-backed item.");
+         if (DEBUG) {
+             checkArgument(MotionEvents.isFingerEvent(e));