1 /*
2  * Copyright 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.recyclerview.selection;
18 
19 import static androidx.annotation.RestrictTo.Scope.LIBRARY;
20 import static androidx.core.util.Preconditions.checkArgument;
21 
22 import android.annotation.SuppressLint;
23 import android.content.Context;
24 import android.os.Bundle;
25 import android.os.Parcelable;
26 import android.util.Log;
27 import android.view.GestureDetector;
28 import android.view.HapticFeedbackConstants;
29 import android.view.InputDevice;
30 import android.view.MotionEvent;
31 
32 import androidx.annotation.DrawableRes;
33 import androidx.annotation.RestrictTo;
34 import androidx.recyclerview.widget.RecyclerView;
35 import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
36 import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
37 
38 import org.jspecify.annotations.NonNull;
39 import org.jspecify.annotations.Nullable;
40 
41 import java.util.Set;
42 
43 /**
44  * SelectionTracker provides support for managing a selection of items in a RecyclerView instance.
45  *
46  * <p>
47  * This class provides support for managing a "primary" set of selected items,
48  * in addition to a "provisional" set of selected items using conventional
49  * {@link java.util.Collections}-like methods.
50  *
51  * <p>
52  * Create an instance of SelectionTracker using {@link Builder SelectionTracker.Builder}.
53  *
54  * <p>
55  * <b>Inspecting the current selection</b>
56  *
57  * <p>
58  * The underlying selection is described by the {@link Selection} class.
59  *
60  * <p>
61  * A live view of the current selection can be obtained using {@link #getSelection}. Changes made
62  * to the selection using SelectionTracker will be immediately reflected in this instance.
63  *
64  * <p>
65  * To obtain a stable snapshot of the selection use {@link #copySelection(MutableSelection)}.
66  *
67  * <p>
68  * Selection state for an individual item can be obtained using {@link #isSelected(Object)}.
69  *
70  * <p>
71  * <b>Provisional Selection</b>
72  *
73  * <p>
74  * Provisional selection exists to address issues where a transitory selection might
75  * momentarily intersect with a previously established selection resulting in a some
76  * or all of the established selection being erased. Such situations may arise
77  * when band selection is being performed in "additive" mode (e.g. SHIFT or CTRL is pressed
78  * on the keyboard prior to mouse down), or when there's an active gesture selection
79  * (which can be initiated by long pressing an unselected item while there is an
80  * existing selection).
81  *
82  * <p>
83  * A provisional selection can be abandoned, or merged into the primary selection.
84  *
85  * <p>
86  * <b>Enforcing selection policies</b>
87  *
88  * <p>
89  * Which items can be selected by the user is a matter of policy in an Application.
90  * Developers supply these policies by way of {@link SelectionPredicate}.
91  *
92  * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
93  */
94 public abstract class SelectionTracker<K> {
95 
96     /**
97      * This value is included in the payload when SelectionTracker notifies RecyclerView
98      * of changes to selection. Look for this value in the {@code payload}
99      * Object argument supplied to
100      * {@link RecyclerView.Adapter#onBindViewHolder
101      * Adapter#onBindViewHolder}.
102      * If present the call is occurring in response to a selection state change.
103      * This would be a good opportunity to animate changes between unselected and selected state.
104      * When state is being restored, this argument will not be present.
105      */
106     public static final String SELECTION_CHANGED_MARKER = "Selection-Changed";
107     private static final String TAG = "SelectionTracker";
108 
109     /**
110      * Adds {@code observer} to be notified when changes to selection occur.
111      *
112      * <p>
113      * Use an observer to track attributes about the selection and
114      * update the UI to reflect the state of the selection. For example, an author
115      * may use an observer to control the enabled status of menu items,
116      * or to initiate {@link android.view.ActionMode}.
117      */
addObserver(@onNull SelectionObserver<K> observer)118     public abstract void addObserver(@NonNull SelectionObserver<K> observer);
119 
120     /** @return true if has a selection */
hasSelection()121     public abstract boolean hasSelection();
122 
123     /**
124      * Returns a Selection object that provides a live view on the current selection.
125      *
126      * @return The current selection.
127      * @see #copySelection(MutableSelection) on how to get a snapshot
128      * of the selection that will not reflect future changes
129      * to selection.
130      */
getSelection()131     public abstract @NonNull Selection<K> getSelection();
132 
133     /**
134      * Updates {@code dest} to reflect the current selection.
135      */
copySelection(@onNull MutableSelection<K> dest)136     public abstract void copySelection(@NonNull MutableSelection<K> dest);
137 
138     /**
139      * @return true if the item specified by its id is selected. Shorthand for
140      * {@code getSelection().contains(K)}.
141      */
isSelected(@ullable K key)142     public abstract boolean isSelected(@Nullable K key);
143 
144     /**
145      * Restores the selected state of specified items. Used in cases such as restore the selection
146      * after rotation etc. Provisional selection is not restored.
147      *
148      * <p>
149      * This affords clients the ability to restore selection from selection saved
150      * in Activity state.
151      *
152      * @param selection selection being restored.
153      * @see StorageStrategy details on selection state support.
154      */
restoreSelection(@onNull Selection<K> selection)155     protected abstract void restoreSelection(@NonNull Selection<K> selection);
156 
157     /**
158      * Clears both primary and provisional selections.
159      *
160      * @return true if primary selection changed.
161      */
clearSelection()162     public abstract boolean clearSelection();
163 
164     /**
165      * Sets the selected state of the specified items if permitted after consulting
166      * SelectionPredicate.
167      */
168     @SuppressLint("LambdaLast")
setItemsSelected(@onNull Iterable<K> keys, boolean selected)169     public abstract boolean setItemsSelected(@NonNull Iterable<K> keys, boolean selected);
170 
171     /**
172      * Attempts to select an item.
173      *
174      * @return true if the item was selected. False if the item could not be selected, or was
175      * was already selected.
176      */
select(@onNull K key)177     public abstract boolean select(@NonNull K key);
178 
179     /**
180      * Attempts to deselect an item.
181      *
182      * @return true if the item was deselected. False if the item could not be deselected, or was
183      * was already un-selected.
184      */
deselect(@onNull K key)185     public abstract boolean deselect(@NonNull K key);
186 
187     @SuppressWarnings("HiddenAbstractMethod")
188     @RestrictTo(LIBRARY)
getAdapterDataObserver()189     protected abstract @NonNull AdapterDataObserver getAdapterDataObserver();
190 
191     /**
192      * Attempts to establish a range selection at {@code position}, selecting the item
193      * at {@code position} if needed.
194      *
195      * @param position The "anchor" position for the range. Subsequent range operations
196      *                 (primarily keyboard and mouse based operations like SHIFT + click)
197      *                 work with the established anchor point to define selection ranges.
198      */
199     @SuppressWarnings("HiddenAbstractMethod")
200     @RestrictTo(LIBRARY)
startRange(int position)201     public abstract void startRange(int position);
202 
203     /**
204      * Sets the end point for the active range selection.
205      *
206      * <p>
207      * This function should only be called when a range selection is active
208      * (see {@link #isRangeActive()}. Items in the range [anchor, end] will be
209      * selected after consulting SelectionPredicate.
210      *
211      * @param position The new end position for the selection range.
212      * @throws IllegalStateException if a range selection is not active. Range selection
213      *                               must have been started by a call to {@link #startRange(int)}.
214      */
215     @SuppressWarnings("HiddenAbstractMethod")
216     @RestrictTo(LIBRARY)
extendRange(int position)217     public abstract void extendRange(int position);
218 
219     /**
220      * Clears an in-progress range selection. Provisional range selection established
221      * using {@link #extendProvisionalRange(int)} will be cleared (unless
222      * {@link #mergeProvisionalSelection()} is called first.)
223      */
224     @SuppressWarnings("HiddenAbstractMethod")
225     @RestrictTo(LIBRARY)
endRange()226     public abstract void endRange();
227 
228     /**
229      * @return Whether or not there is a current range selection active.
230      */
231     @SuppressWarnings("HiddenAbstractMethod")
232     @RestrictTo(LIBRARY)
isRangeActive()233     public abstract boolean isRangeActive();
234 
235     /**
236      * Establishes the "anchor" at which a selection range begins. This "anchor" is consulted
237      * when determining how to extend, and modify selection ranges. Calling this when a
238      * range selection is active will reset the range selection.
239      *
240      * TODO: Reconcile this with startRange. Maybe just docs need to be updated.
241      *
242      * @param position the anchor position. Must already be selected.
243      */
244     @SuppressWarnings("HiddenAbstractMethod")
245     @RestrictTo(LIBRARY)
anchorRange(int position)246     public abstract void anchorRange(int position);
247 
248     /**
249      * Creates a provisional selection from anchor to {@code position}.
250      *
251      * @param position the end point.
252      */
253     @SuppressWarnings("HiddenAbstractMethod")
254     @RestrictTo(LIBRARY)
extendProvisionalRange(int position)255     protected abstract void extendProvisionalRange(int position);
256 
257     /**
258      * Sets the provisional selection, replacing any existing selection.
259      */
260     @SuppressWarnings("HiddenAbstractMethod")
261     @RestrictTo(LIBRARY)
setProvisionalSelection(@onNull Set<K> newSelection)262     protected abstract void setProvisionalSelection(@NonNull Set<K> newSelection);
263 
264     /**
265      * Clears any existing provisional selection
266      */
267     @SuppressWarnings("HiddenAbstractMethod")
268     @RestrictTo(LIBRARY)
clearProvisionalSelection()269     protected abstract void clearProvisionalSelection();
270 
271     /**
272      * Converts the provisional selection into primary selection, then clears
273      * provisional selection.
274      */
275     @SuppressWarnings("HiddenAbstractMethod")
276     @RestrictTo(LIBRARY)
mergeProvisionalSelection()277     protected abstract void mergeProvisionalSelection();
278 
279     /**
280      * Preserves selection, if any. Call this method from Activity#onSaveInstanceState
281      *
282      * @param state Bundle instance supplied to onSaveInstanceState.
283      */
onSaveInstanceState(@onNull Bundle state)284     public abstract void onSaveInstanceState(@NonNull Bundle state);
285 
286     /**
287      * Restores selection from previously saved state. Call this method from
288      * Activity#onCreate.
289      *
290      * @param state Bundle instance supplied to onCreate.
291      */
onRestoreInstanceState(@ullable Bundle state)292     public abstract void onRestoreInstanceState(@Nullable Bundle state);
293 
294     /**
295      * Observer class providing access to information about Selection state changes.
296      *
297      * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
298      */
299     public abstract static class SelectionObserver<K> {
300 
301         /**
302          * Called when the state of an item has been changed.
303          */
onItemStateChanged(@onNull K key, boolean selected)304         public void onItemStateChanged(@NonNull K key, boolean selected) {
305         }
306 
307         /**
308          * Called when Selection is cleared.
309          * TODO(smckay): Make public in a future public API.
310          */
311         @RestrictTo(LIBRARY)
onSelectionCleared()312         protected void onSelectionCleared() {
313         }
314 
315         /**
316          * Called when the underlying data set has changed. After this method is called
317          * SelectionTracker will traverse the existing selection,
318          * calling {@link #onItemStateChanged(K, boolean)} for each selected item,
319          * and deselecting any items that cannot be selected given the updated data-set
320          * (and after consulting SelectionPredicate).
321          */
onSelectionRefresh()322         public void onSelectionRefresh() {
323         }
324 
325         /**
326          * Called immediately after completion of any set of changes, excluding
327          * those resulting in calls {@link #onSelectionRestored()}.
328          */
onSelectionChanged()329         public void onSelectionChanged() {
330         }
331 
332         /**
333          * Called immediately after selection is restored.
334          * {@link #onItemStateChanged(K, boolean)} will *not* be called
335          * for individual items in the selection.
336          */
onSelectionRestored()337         public void onSelectionRestored() {
338         }
339     }
340 
341     /**
342      * Implement SelectionPredicate to control when items can be selected or unselected.
343      * See {@link Builder#withSelectionPredicate(SelectionPredicate)}.
344      *
345      * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
346      */
347     public abstract static class SelectionPredicate<K> {
348 
349         /**
350          * Validates a change to selection for a specific key.
351          *
352          * @param key       the item key
353          * @param nextState the next potential selected/unselected state
354          * @return true if the item at {@code id} can be set to {@code nextState}.
355          */
canSetStateForKey(@onNull K key, boolean nextState)356         public abstract boolean canSetStateForKey(@NonNull K key, boolean nextState);
357 
358         /**
359          * Validates a change to selection for a specific position. If necessary
360          * use {@link ItemKeyProvider} to identy associated key.
361          *
362          * @param position  the item position
363          * @param nextState the next potential selected/unselected state
364          * @return true if the item at {@code id} can be set to {@code nextState}.
365          */
canSetStateAtPosition(int position, boolean nextState)366         public abstract boolean canSetStateAtPosition(int position, boolean nextState);
367 
368         /**
369          * Permits restriction to single selection mode. Single selection mode has
370          * unique behaviors in that it'll deselect an item already selected
371          * in order to select the new item.
372          *
373          * <p>
374          * In order to limit the number of items that can be selected,
375          * use {@link #canSetStateForKey(Object, boolean)} and
376          * {@link #canSetStateAtPosition(int, boolean)}.
377          *
378          * @return true if more than a single item can be selected.
379          */
canSelectMultiple()380         public abstract boolean canSelectMultiple();
381     }
382 
383     /**
384      * Builder is the primary mechanism for creating a {@link SelectionTracker} that
385      * can be used with your RecyclerView. Once installed, users will be able to create and
386      * manipulate a selection of items in a RecyclerView instance using a variety of
387      * intuitive techniques like tap, gesture, and mouse-based band selection (aka 'lasso').
388      *
389      * <p>
390      * Building a bare-bones instance:
391      *
392      * <pre>{@code
393      * SelectionTracker<Uri> tracker = new SelectionTracker.Builder<>(
394      *         "my-uri-selection",
395      *         recyclerView,
396      *         new YourItemKeyProvider(recyclerView.getAdapter()),
397      *         new YourItemDetailsLookup(recyclerView),
398      *         StorageStrategy.createParcelableStorage(Uri.class))
399      *     .build();
400      * }</pre>
401      *
402      * <p>
403      * <b>Restricting which items can be selected and limiting selection size</b>
404      *
405      * <p>
406      * {@link SelectionPredicate} and
407      * {@link SelectionTracker.Builder#withSelectionPredicate(SelectionPredicate)}
408      * together provide a mechanism for restricting which items can be selected and
409      * limiting selection size. Use {@link SelectionPredicates#createSelectSingleAnything()}
410      * for single-selection, or write your own {@link SelectionPredicate} if other
411      * constraints are required.
412      *
413      * <pre>{@code
414      * SelectionTracker<String> tracker = new SelectionTracker.Builder<>(
415      *         "my-string-selection",
416      *         recyclerView,
417      *         new YourItemKeyProvider(recyclerView.getAdapter()),
418      *         new YourItemDetailsLookup(recyclerView),
419      *         StorageStrategy.createStringStorage())
420      *     .withSelectionPredicate(SelectionPredicates#createSelectSingleAnything())
421      *     .build();
422      * }</pre>
423      *
424      * <p>
425      * <b>Retaining state across Android lifecycle events</b>
426      *
427      * <p>
428      * Support for storage/persistence of selection must be configured and invoked manually
429      * owing to its reliance on Activity lifecycle events.
430      * Failure to include support for selection storage will result in selection
431      * being lost when the Activity receives a configuration change (e.g. rotation),
432      * or when the application is paused or stopped. For this reason
433      * {@link StorageStrategy} is a required argument to obtain a {@link Builder}
434      * instance.
435      *
436      * <p>
437      * <b>Key Type</b>
438      *
439      * <p>
440      * A developer must decide on the key type used to identify selected items.
441      * Support is provided for three types: {@link Parcelable}, {@link String}, and {@link Long}.
442      *
443      * <p>
444      * {@link Parcelable}: Any Parcelable type can be used as the selection key. This is especially
445      * useful in conjunction with {@link android.net.Uri} as the Android URI implementation is both
446      * parcelable and makes for a natural stable selection key for values represented by
447      * the Android Content Provider framework. If items in your view are associated with
448      * stable {@code content://} uris, you should use Uri for your key type.
449      *
450      * <p>
451      * {@link String}: Use String when a string based stable identifier is available.
452      *
453      * <p>
454      * {@link Long}: Use Long when a project is already employing RecyclerView's built-in
455      * support for stable ids. In this case you may choose to use {@link StableIdKeyProvider}
456      * to supply selection keys to the SelectionTracker based on data already accessible
457      * in RecyclerView and it's Adapter.
458      *
459      * See {@link StableIdKeyProvider} for important details and limitations (<i>and a suggestion
460      * that you might just want to write your own {@link ItemKeyProvider}. It's easy!</i>)
461      * See the "Gotchas" selection below for details on selection size limits.
462      *
463      * <p>
464      * Usage:
465      *
466      * <pre>{@code
467      * private SelectionTracker<Uri> tracker;
468      *
469      * public void onCreate(Bundle savedInstanceState) {
470      *   if (savedInstanceState != null) {
471      *     tracker.onRestoreInstanceState(savedInstanceState);
472      *   }
473      * }
474      *
475      * protected void onSaveInstanceState(Bundle outState) {
476      *   super.onSaveInstanceState(outState);
477      *   tracker.onSaveInstanceState(outState);
478      * }
479      * }</pre>
480      *
481      * <p>
482      * <b>Gotchas</b>
483      *
484      * <p>TransactionTooLargeException:
485      *
486      * <p>Many factors affect the maximum number of items that can be persisted when the
487      * application is paused or stopped. Unfortunately that number is not deterministic as it
488      * depends on the size of the key type used for selection, the number of selected items, and
489      * external demand on system resources. For that reason it is best to use the smallest viable
490      * key type, and to enforce a limit on the number of items that can be selected.
491      *
492      * <p>Furthermore the inability to persist a selection during a lifecycle event will result
493      * in a android.os.{@link android.os.TransactionTooLargeException}. See
494      * http://issuetracker.google.com/168706011 for details.
495      *
496      * <p>ItemTouchHelper
497      *
498      * <p>When using {@link SelectionTracker} along side an
499      * {@link androidx.recyclerview.widget.ItemTouchHelper} with the same RecyclerView instance
500      * the SelectionTracker instance must be created and installed before the ItemTouchHelper.
501      * Failure to do so will result in unintended selections during item drag operations, and
502      * possibly other situations.
503      *
504      * @param <K> Selection key type. Built in support is provided for {@link String},
505      *            {@link Long}, and {@link Parcelable}. {@link StorageStrategy}
506      *            provides factory methods for each type:
507      *            {@link StorageStrategy#createStringStorage()},
508      *            {@link StorageStrategy#createParcelableStorage(Class)},
509      *            {@link StorageStrategy#createLongStorage()}
510      */
511     public static final class Builder<K> {
512 
513         final RecyclerView mRecyclerView;
514         private final RecyclerView.Adapter<?> mAdapter;
515         private final Context mContext;
516         private final String mSelectionId;
517         private final StorageStrategy<K> mStorage;
518 
519         SelectionPredicate<K> mSelectionPredicate =
520                 SelectionPredicates.createSelectAnything();
521         private OperationMonitor mMonitor = new OperationMonitor();
522         private ItemKeyProvider<K> mKeyProvider;
523         private ItemDetailsLookup<K> mDetailsLookup;
524 
525         private FocusDelegate<K> mFocusDelegate = FocusDelegate.stub();
526 
527         private OnItemActivatedListener<K> mOnItemActivatedListener;
528         private OnDragInitiatedListener mOnDragInitiatedListener;
529         private OnContextClickListener mOnContextClickListener;
530 
531         private BandPredicate mBandPredicate;
532         private int mBandOverlayId = R.drawable.selection_band_overlay;
533 
534         // TODO(b/144500333): Remove support for overriding gesture and pointer tooltypes.
535         private int[] mGestureToolTypes = new int[]{
536                 MotionEvent.TOOL_TYPE_FINGER
537         };
538 
539         private int[] mPointerToolTypes = new int[]{
540                 MotionEvent.TOOL_TYPE_MOUSE
541         };
542 
543         /**
544          * Creates a new SelectionTracker.Builder useful for configuring and creating
545          * a new SelectionTracker for use with your {@link RecyclerView}.
546          *
547          * @param selectionId   A unique string identifying this selection in the context
548          *                      of the activity or fragment.
549          * @param recyclerView  the owning RecyclerView
550          * @param keyProvider   the source of selection keys
551          * @param detailsLookup the source of information about RecyclerView items.
552          * @param storage       Strategy for type-safe storage of selection state in
553          *                      {@link Bundle}.
554          */
Builder( @onNull String selectionId, @NonNull RecyclerView recyclerView, @NonNull ItemKeyProvider<K> keyProvider, @NonNull ItemDetailsLookup<K> detailsLookup, @NonNull StorageStrategy<K> storage)555         public Builder(
556                 @NonNull String selectionId,
557                 @NonNull RecyclerView recyclerView,
558                 @NonNull ItemKeyProvider<K> keyProvider,
559                 @NonNull ItemDetailsLookup<K> detailsLookup,
560                 @NonNull StorageStrategy<K> storage) {
561 
562             checkArgument(selectionId != null);
563             checkArgument(!selectionId.trim().isEmpty());
564             checkArgument(recyclerView != null);
565 
566             mSelectionId = selectionId;
567             mRecyclerView = recyclerView;
568             mContext = recyclerView.getContext();
569             mAdapter = recyclerView.getAdapter();
570 
571             checkArgument(mAdapter != null);
572             checkArgument(keyProvider != null);
573             checkArgument(detailsLookup != null);
574             checkArgument(storage != null);
575 
576             mDetailsLookup = detailsLookup;
577             mKeyProvider = keyProvider;
578             mStorage = storage;
579 
580             mBandPredicate = new BandPredicate.NonDraggableArea(mRecyclerView, detailsLookup);
581         }
582 
583         /**
584          * Install selection predicate.
585          *
586          * @param predicate the predicate to be used.
587          * @return this
588          */
withSelectionPredicate( @onNull SelectionPredicate<K> predicate)589         public @NonNull Builder<K> withSelectionPredicate(
590                 @NonNull SelectionPredicate<K> predicate) {
591 
592             checkArgument(predicate != null);
593             mSelectionPredicate = predicate;
594             return this;
595         }
596 
597         /**
598          * Add operation monitor allowing access to information about active
599          * operations (like band selection and gesture selection).
600          *
601          * @param monitor the monitor to be used
602          * @return this
603          */
withOperationMonitor( @onNull OperationMonitor monitor)604         public @NonNull Builder<K> withOperationMonitor(
605                 @NonNull OperationMonitor monitor) {
606 
607             checkArgument(monitor != null);
608             mMonitor = monitor;
609             return this;
610         }
611 
612         /**
613          * Add focus delegate to interact with selection related focus changes.
614          *
615          * @param delegate the delegate to be used
616          * @return this
617          */
withFocusDelegate(@onNull FocusDelegate<K> delegate)618         public @NonNull Builder<K> withFocusDelegate(@NonNull FocusDelegate<K> delegate) {
619             checkArgument(delegate != null);
620             mFocusDelegate = delegate;
621             return this;
622         }
623 
624         /**
625          * Adds an item activation listener. Respond to taps/enter/double-click on items.
626          *
627          * @param listener the listener to be used
628          * @return this
629          */
withOnItemActivatedListener( @onNull OnItemActivatedListener<K> listener)630         public @NonNull Builder<K> withOnItemActivatedListener(
631                 @NonNull OnItemActivatedListener<K> listener) {
632 
633             checkArgument(listener != null);
634 
635             mOnItemActivatedListener = listener;
636             return this;
637         }
638 
639         /**
640          * Adds a context click listener. Respond to right-click.
641          *
642          * @param listener the listener to be used
643          * @return this
644          */
withOnContextClickListener( @onNull OnContextClickListener listener)645         public @NonNull Builder<K> withOnContextClickListener(
646                 @NonNull OnContextClickListener listener) {
647 
648             checkArgument(listener != null);
649 
650             mOnContextClickListener = listener;
651             return this;
652         }
653 
654         /**
655          * Adds a drag initiated listener. Add support for drag and drop.
656          *
657          * @param listener the listener to be used
658          * @return this
659          */
withOnDragInitiatedListener( @onNull OnDragInitiatedListener listener)660         public @NonNull Builder<K> withOnDragInitiatedListener(
661                 @NonNull OnDragInitiatedListener listener) {
662 
663             checkArgument(listener != null);
664 
665             mOnDragInitiatedListener = listener;
666             return this;
667         }
668 
669         /**
670          * Replaces default tap and gesture tool-types. Defaults are:
671          * {@link MotionEvent#TOOL_TYPE_FINGER}.
672          *
673          * @param toolTypes the tool types to be used
674          * @return this
675          * @deprecated GestureSelection is best bound to {@link MotionEvent#TOOL_TYPE_FINGER},
676          * and only that tool type. This method will be removed in a future release.
677          */
678         @Deprecated
withGestureTooltypes(int @NonNull ... toolTypes)679         public @NonNull Builder<K> withGestureTooltypes(int @NonNull ... toolTypes) {
680             Log.w(TAG, "Setting gestureTooltypes is likely to result in unexpected behavior.");
681             mGestureToolTypes = toolTypes;
682             return this;
683         }
684 
685         /**
686          * Replaces default band overlay.
687          *
688          * @return this
689          */
withBandOverlay(@rawableRes int bandOverlayId)690         public @NonNull Builder<K> withBandOverlay(@DrawableRes int bandOverlayId) {
691             mBandOverlayId = bandOverlayId;
692             return this;
693         }
694 
695         /**
696          * Replaces default band predicate.
697          *
698          * @return this
699          */
withBandPredicate(@onNull BandPredicate bandPredicate)700         public @NonNull Builder<K> withBandPredicate(@NonNull BandPredicate bandPredicate) {
701             mBandPredicate = bandPredicate;
702             return this;
703         }
704 
705         /**
706          * Replaces default pointer tool-types. Pointer tools
707          * are associated with band selection, and certain
708          * drag and drop behaviors. Defaults are:
709          * {@link MotionEvent#TOOL_TYPE_MOUSE}.
710          *
711          * @param toolTypes the tool types to be used
712          * @return this
713          * @deprecated PointerSelection is best bound to {@link MotionEvent#TOOL_TYPE_MOUSE},
714          * and only that tool type. This method will be removed in a future release.
715          */
716         @Deprecated
withPointerTooltypes(int @NonNull ... toolTypes)717         public @NonNull Builder<K> withPointerTooltypes(int @NonNull ... toolTypes) {
718             Log.w(TAG, "Setting pointerTooltypes is likely to result in unexpected behavior.");
719             mPointerToolTypes = toolTypes;
720             return this;
721         }
722 
723         /**
724          * Prepares and returns a SelectionTracker.
725          *
726          * @return this
727          */
build()728         public @NonNull SelectionTracker<K> build() {
729 
730             DefaultSelectionTracker<K> tracker = new DefaultSelectionTracker<>(
731                     mSelectionId, mKeyProvider, mSelectionPredicate, mStorage);
732 
733             // Event glue between RecyclerView and SelectionTracker keeps the classes separate
734             // so that a SelectionTracker can be shared across RecyclerView instances that
735             // represent the same data in different ways.
736             EventBridge.install(mAdapter, tracker, mKeyProvider, mRecyclerView::post);
737 
738             // Scroller is stateful and can be reset, but we don't manage it directly.
739             // GestureSelectionHelper will reset scroller when it is reset.
740             AutoScroller scroller =
741                     new ViewAutoScroller(ViewAutoScroller.createScrollHost(mRecyclerView));
742 
743             // Setup basic input handling, with the touch handler as the default consumer
744             // of events. If mouse handling is configured as well, the mouse input
745             // related handlers will intercept mouse input events.
746 
747             // GestureRouter is responsible for routing GestureDetector events
748             // to tool-type specific handlers.
749             GestureRouter<MotionInputHandler<K>> gestureRouter = new GestureRouter<>();
750 
751             // GestureDetector cancels itself in response to ACTION_CANCEL events.
752             GestureDetector gestureDetector = new GestureDetector(mContext, gestureRouter);
753 
754             // GestureSelectionHelper provides logic that interprets a combination
755             // of motions and gestures in order to provide fluid "long-press and drag"
756             // finger driven selection support.
757             final GestureSelectionHelper gestureSelectionHelper = GestureSelectionHelper.create(
758                     tracker, mSelectionPredicate, mRecyclerView, scroller, mMonitor);
759 
760             // EventRouter receives events for RecyclerView, dispatching to handlers
761             // registered by tool-type.
762             EventRouter eventRouter = new EventRouter();
763             GestureDetectorWrapper gestureDetectorWrapper =
764                     new GestureDetectorWrapper(gestureDetector);
765 
766             // Temp fix for b/166836317.
767             // TODO: Add support for multiple listeners per tool type to EventRouter, then
768             //  register backstop with primary router.
769             EventRouter backstopRouter = new EventRouter();
770             EventBackstop backstop = new EventBackstop();
771             DisallowInterceptFilter backstopWrapper = new DisallowInterceptFilter(backstop);
772             backstopRouter.set(new ToolSourceKey(MotionEvent.TOOL_TYPE_UNKNOWN), backstopWrapper);
773 
774             // Finally hook the framework up to listening to RecycleView events.
775             mRecyclerView.addOnItemTouchListener(eventRouter);
776             mRecyclerView.addOnItemTouchListener(gestureDetectorWrapper);
777             mRecyclerView.addOnItemTouchListener(backstopRouter);
778 
779             // Reset manager listens for cancel events from RecyclerView. In response to that it
780             // advises other classes it is time to reset state.
781             ResetManager<K> resetMgr = new ResetManager<>();
782 
783             // Register ResetManager to:
784             //
785             // 1. Monitor selection reset which can be invoked by clients in response
786             //    to back key press and some application lifecycle events.
787             tracker.addObserver(resetMgr.getSelectionObserver());
788 
789             // ...and  2. Monitor ACTION_CANCEL events (which arrive exclusively
790             // via TOOL_TYPE_UNKNOWN).
791             //
792             // CAUTION! Registering resetMgr directly with RecyclerView#addOnItemTouchListener
793             // will not work as expected. Once EventRouter returns true, RecyclerView will
794             // no longer dispatch any events to other listeners for the duration of the
795             // stream, not even ACTION_CANCEL events.
796             eventRouter.set(new ToolSourceKey(MotionEvent.TOOL_TYPE_UNKNOWN),
797                     resetMgr.getInputListener());
798 
799             // Finally register all of the Resettables.
800             resetMgr.addResetHandler(tracker);
801             resetMgr.addResetHandler(mMonitor.asResettable());
802             resetMgr.addResetHandler(gestureSelectionHelper);
803             resetMgr.addResetHandler(gestureDetectorWrapper);
804             resetMgr.addResetHandler(eventRouter);
805             resetMgr.addResetHandler(backstopRouter);
806             resetMgr.addResetHandler(backstop);
807             resetMgr.addResetHandler(backstopWrapper);
808 
809             // But before you move on, there's more work to do. Event plumbing has been
810             // installed, but we haven't registered any of our helpers or callbacks.
811             // Helpers contain predefined logic converting events into selection related events.
812             // Callbacks provide developers the ability to reponspond to other types of
813             // events (like "activate" a tapped item). This is broken up into two main
814             // suites, one for "touch" and one for "mouse", though both can and should (usually)
815             // be configured to handle other types of input (to satisfy user expectation).);
816 
817             // Internally, the code doesn't permit nullable listeners, so we lazily
818             // initialize stub instances if the developer didn't supply a real listener.
819             mOnDragInitiatedListener = (mOnDragInitiatedListener != null)
820                     ? mOnDragInitiatedListener
821                     : new OnDragInitiatedListener() {
822                         @Override
823                         public boolean onDragInitiated(@NonNull MotionEvent e) {
824                             return false;
825                         }
826                     };
827 
828             mOnItemActivatedListener = (mOnItemActivatedListener != null)
829                     ? mOnItemActivatedListener
830                     : new OnItemActivatedListener<K>() {
831                         @Override
832                         public boolean onItemActivated(
833                                 ItemDetailsLookup.@NonNull ItemDetails<K> item,
834                                 @NonNull MotionEvent e) {
835                             return false;
836                         }
837                     };
838 
839             mOnContextClickListener = (mOnContextClickListener != null)
840                     ? mOnContextClickListener
841                     : new OnContextClickListener() {
842                         @Override
843                         public boolean onContextClick(@NonNull MotionEvent e) {
844                             return false;
845                         }
846                     };
847 
848             // Provides high level glue for binding touch events
849             // and gestures to selection framework.
850             TouchInputHandler<K> touchHandler = new TouchInputHandler<>(
851                     tracker,
852                     mKeyProvider,
853                     mDetailsLookup,
854                     mSelectionPredicate,
855                     gestureSelectionHelper::start,
856                     mOnDragInitiatedListener,
857                     mOnItemActivatedListener,
858                     mFocusDelegate,
859                     new Runnable() {
860                         @Override
861                         public void run() {
862                             mRecyclerView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
863                         }
864                     },
865                     // Provide temporary glue to address b/166836317
866                     backstop::onLongPress);
867 
868             for (int toolType : mGestureToolTypes) {
869                 ToolSourceKey key = new ToolSourceKey(toolType);
870                 gestureRouter.register(key, touchHandler);
871                 eventRouter.set(key, gestureSelectionHelper);
872             }
873 
874             // Provides high level glue for binding mouse events and gestures
875             // to selection framework.
876             MouseInputHandler<K> mouseHandler = new MouseInputHandler<>(
877                     tracker,
878                     mKeyProvider,
879                     mDetailsLookup,
880                     mOnContextClickListener,
881                     mOnItemActivatedListener,
882                     mFocusDelegate);
883 
884             for (int toolType : mPointerToolTypes) {
885                 gestureRouter.register(new ToolSourceKey(toolType), mouseHandler);
886             }
887 
888             ToolSourceKey touchpadKey = new ToolSourceKey(MotionEvent.TOOL_TYPE_FINGER,
889                     InputDevice.SOURCE_MOUSE);
890             gestureRouter.register(touchpadKey, mouseHandler);
891 
892             BandSelectionHelper<K> bandHelper = null;
893 
894             // Band selection not supported in single select mode, or when key access
895             // is limited to anything less than the entire corpus.
896             if (mKeyProvider.hasAccess(ItemKeyProvider.SCOPE_MAPPED)
897                     && mSelectionPredicate.canSelectMultiple()) {
898                 // BandSelectionHelper provides support for band selection on-top of a RecyclerView
899                 // instance. Given the recycling nature of RecyclerView BandSelectionController
900                 // necessarily models and caches list/grid information as the user's pointer
901                 // interacts with the item in the RecyclerView. Selectable items that intersect
902                 // with the band, both on and off screen, are selected.
903                 bandHelper = BandSelectionHelper.create(
904                         mRecyclerView,
905                         scroller,
906                         mBandOverlayId,
907                         mKeyProvider,
908                         tracker,
909                         mSelectionPredicate,
910                         mBandPredicate,
911                         mFocusDelegate,
912                         mMonitor);
913 
914                 resetMgr.addResetHandler(bandHelper);
915             }
916 
917             OnItemTouchListener pointerEventHandler = new PointerDragEventInterceptor(
918                     mDetailsLookup, mOnDragInitiatedListener, bandHelper);
919 
920             eventRouter.set(new ToolSourceKey(MotionEvent.TOOL_TYPE_MOUSE), pointerEventHandler);
921             eventRouter.set(touchpadKey, pointerEventHandler);
922 
923             return tracker;
924         }
925     }
926 }
927