Updated AndroidX

master
M66B 4 months ago
parent c5d64baefc
commit 95e023399d

@ -551,16 +551,16 @@ dependencies {
def appcompat_version = "1.7.0" def appcompat_version = "1.7.0"
def emoji_version = "1.5.0" def emoji_version = "1.5.0"
def flatbuffers_version = "2.0.0" def flatbuffers_version = "2.0.0"
def activity_version = "1.10.0" // 1.11.0-rc01 def activity_version = "1.10.0" // 1.11.0-rc01//1.12.0-alpha01
def fragment_version = "1.8.6" def fragment_version = "1.8.7"
def windows_version = "1.3.0" // 1.4.0-rc02/1.5.0-alpha02 def windows_version = "1.4.0" // 1.5.0-alpha02
def webkit_version = "1.13.0" // 1.14.0-beta01 def webkit_version = "1.13.0" // 1.14.0-rc01
def recyclerview_version = "1.4.0" def recyclerview_version = "1.4.0"
def coordinatorlayout_version = "1.2.0" // 1.3.0-rc01 def coordinatorlayout_version = "1.2.0" // 1.3.0-rc01
def constraintlayout_version = "2.2.0" def constraintlayout_version = "2.2.0"
def viewpager_version = "1.1.0-beta01" // 1.1.0 def viewpager_version = "1.1.0-beta01" // 1.1.0
def material_version = "1.12.0" // 1.13.0-alpha03 def material_version = "1.12.0" // 1.13.0-alpha03
def browser_version = "1.8.0" // 1.9.0-alpha03 def browser_version = "1.8.0" // 1.9.0-alpha04
def lbm_version = "1.1.0" def lbm_version = "1.1.0"
def swiperefresh_version = "1.2.0-beta01" def swiperefresh_version = "1.2.0-beta01"
def documentfile_version = "1.1.0" def documentfile_version = "1.1.0"
@ -573,7 +573,7 @@ dependencies {
def preference_version = "1.2.1" def preference_version = "1.2.1"
def work_version = "2.10.1" def work_version = "2.10.1"
def exif_version = "1.4.1" def exif_version = "1.4.1"
def biometric_version = "1.2.0-alpha05" // 1.4.0-alpha03 def biometric_version = "1.2.0-alpha05" // 1.4.0-alpha04
def billingclient_version = "6.0.1" // 6.2.0 def billingclient_version = "6.0.1" // 6.2.0
def playservicesbasement_version = "18.5.0"; def playservicesbasement_version = "18.5.0";
def transparency_version = "2.5.73" def transparency_version = "2.5.73"
@ -644,7 +644,7 @@ dependencies {
// https://mvnrepository.com/artifact/androidx.recyclerview/recyclerview // https://mvnrepository.com/artifact/androidx.recyclerview/recyclerview
// https://mvnrepository.com/artifact/androidx.recyclerview/recyclerview-selection // https://mvnrepository.com/artifact/androidx.recyclerview/recyclerview-selection
implementation "androidx.recyclerview:recyclerview:$recyclerview_version" implementation "androidx.recyclerview:recyclerview:$recyclerview_version"
//implementation "androidx.recyclerview:recyclerview-selection:1.1.0" // 1.2.0-rc01 //implementation "androidx.recyclerview:recyclerview-selection:1.2.0"
// https://mvnrepository.com/artifact/androidx.coordinatorlayout/coordinatorlayout // https://mvnrepository.com/artifact/androidx.coordinatorlayout/coordinatorlayout
implementation "androidx.coordinatorlayout:coordinatorlayout:$coordinatorlayout_version" implementation "androidx.coordinatorlayout:coordinatorlayout:$coordinatorlayout_version"
@ -862,7 +862,7 @@ dependencies {
implementation "com.github.seancfoley:ipaddress:$ipaddress_version" implementation "com.github.seancfoley:ipaddress:$ipaddress_version"
// https://mvnrepository.com/artifact/androidx.car.app/app?repo=google // https://mvnrepository.com/artifact/androidx.car.app/app?repo=google
// implementation "androidx.car.app:app:1.4.0-rc02" // implementation "androidx.car.app:app:1.4.0-rc02" // 1.8.0-alpha01
// https://github.com/square/leakcanary // https://github.com/square/leakcanary
// https://square.github.io/leakcanary/getting_started/ // https://square.github.io/leakcanary/getting_started/

@ -17,21 +17,18 @@
package androidx.recyclerview.selection; package androidx.recyclerview.selection;
import static androidx.annotation.RestrictTo.Scope.LIBRARY; import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE;
import android.graphics.Point; import android.graphics.Point;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import org.jspecify.annotations.NonNull;
/** /**
* Provides support for auto-scrolling a view. * Provides support for auto-scrolling a view.
* *
* @hide
*/ */
@RestrictTo(LIBRARY) @RestrictTo(LIBRARY)
@VisibleForTesting(otherwise = PACKAGE_PRIVATE)
public abstract class AutoScroller { public abstract class AutoScroller {
/** /**

@ -21,12 +21,12 @@ import static androidx.core.util.Preconditions.checkArgument;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import org.jspecify.annotations.NonNull;
/** /**
* Provides a means of controlling when and where band selection can be initiated. * Provides a means of controlling when and where band selection can be initiated.
* *
@ -132,7 +132,7 @@ public abstract class BandPredicate {
return false; return false;
} }
@Nullable ItemDetailsLookup.ItemDetails<?> details = ItemDetailsLookup.ItemDetails<?> details =
mDetailsLookup.getItemDetails(e); mDetailsLookup.getItemDetails(e);
return (details == null) || !details.inDragRegion(e); return (details == null) || !details.inDragRegion(e);
} }

@ -26,13 +26,14 @@ import android.util.Log;
import android.view.MotionEvent; import android.view.MotionEvent;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate; import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener; import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
import androidx.recyclerview.widget.RecyclerView.OnScrollListener; import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import java.util.Set; import java.util.Set;
/** /**
@ -210,7 +211,7 @@ class BandSelectionHelper<K> implements OnItemTouchListener, Resettable {
} }
// We shouldn't get any events in this method when band select is not active, // We shouldn't get any events in this method when band select is not active,
// but it turns some guests show up late to the party. // but it turns out some guests show up late to the party.
// Probably happening when a re-layout is happening to the ReyclerView (ie. Pull-To-Refresh) // Probably happening when a re-layout is happening to the ReyclerView (ie. Pull-To-Refresh)
if (!isActive()) { if (!isActive()) {
return; return;
@ -254,6 +255,8 @@ class BandSelectionHelper<K> implements OnItemTouchListener, Resettable {
mLock.start(); mLock.start();
mFocusDelegate.clearFocus(); mFocusDelegate.clearFocus();
mOrigin = origin; mOrigin = origin;
mCurrentPosition = origin;
// NOTE: Pay heed that resizeBand modifies the y coordinates // NOTE: Pay heed that resizeBand modifies the y coordinates
// in onScrolled. Not sure if model expects this. If not // in onScrolled. Not sure if model expects this. If not
// it should be defending against this. // it should be defending against this.
@ -323,6 +326,22 @@ class BandSelectionHelper<K> implements OnItemTouchListener, Resettable {
return; return;
} }
// mOrigin and mCurrentPosition should never be null when onScrolled is called,
// but "never say never" increasingly looks like a motto to follow.
// For this reason we guard those specific cases and provide a clear
// error message in the logs.
if (mOrigin == null) {
Log.e(TAG, "onScrolled called while mOrigin null.");
if (DEBUG) throw new IllegalStateException("mOrigin is null.");
return;
}
if (mCurrentPosition == null) {
Log.e(TAG, "onScrolled called while mCurrentPosition null.");
if (DEBUG) throw new IllegalStateException("mCurrentPosition is null.");
return;
}
// Adjust the y-coordinate of the origin the opposite number of pixels so that the // Adjust the y-coordinate of the origin the opposite number of pixels so that the
// origin remains in the same place relative to the view's items. // origin remains in the same place relative to the view's items.
mOrigin.y -= dy; mOrigin.y -= dy;

@ -25,7 +25,6 @@ import android.graphics.drawable.Drawable;
import android.view.View; import android.view.View;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate; import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate;
import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.GridLayoutManager;
@ -33,6 +32,8 @@ import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.ItemDecoration; import androidx.recyclerview.widget.RecyclerView.ItemDecoration;
import androidx.recyclerview.widget.RecyclerView.OnScrollListener; import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
import org.jspecify.annotations.NonNull;
/** /**
* RecyclerView backed {@link BandSelectionHelper.BandHost}. * RecyclerView backed {@link BandSelectionHelper.BandHost}.
*/ */

@ -24,14 +24,15 @@ import static androidx.recyclerview.selection.Shared.DEBUG;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import androidx.recyclerview.selection.Range.RangeType; import androidx.recyclerview.selection.Range.RangeType;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver; import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -46,7 +47,6 @@ import java.util.Set;
* {@link SelectionPredicate#canSelectMultiple()}. * {@link SelectionPredicate#canSelectMultiple()}.
* *
* @param <K> Selection key type. @see {@link StorageStrategy} for supported types. * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
* @hide
*/ */
@RestrictTo(LIBRARY) @RestrictTo(LIBRARY)
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@ -191,7 +191,7 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> implements R
private Selection<K> clearSelectionQuietly() { private Selection<K> clearSelectionQuietly() {
mRange = null; mRange = null;
MutableSelection<K> prevSelection = new MutableSelection(); MutableSelection<K> prevSelection = new MutableSelection<>();
if (hasSelection()) { if (hasSelection()) {
copySelection(prevSelection); copySelection(prevSelection);
mSelection.clear(); mSelection.clear();
@ -394,6 +394,11 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> implements R
@SuppressWarnings({"WeakerAccess", "unchecked"}) /* synthetic access */ @SuppressWarnings({"WeakerAccess", "unchecked"}) /* synthetic access */
void onDataSetChanged() { void onDataSetChanged() {
if (mSelection.isEmpty()) {
Log.d(TAG, "Ignoring onDataSetChange. No active selection.");
return;
}
//mSelection.clearProvisionalSelection(); //mSelection.clearProvisionalSelection();
notifySelectionRefresh(); notifySelectionRefresh();
@ -401,10 +406,12 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> implements R
List<K> toRemove = null; List<K> toRemove = null;
for (K key : mSelection) { for (K key : mSelection) {
// If the underlying data set has changed, before restoring // If the underlying data set has changed, before restoring
// selection we must re-verify that it can be selected. // selection we must re-verify that the items are present
// and if so, can still be selected.
// Why? Because if the dataset has changed, then maybe the // Why? Because if the dataset has changed, then maybe the
// selectability of an item has changed. // selectability of an item has changed, or item disappeared.
if (!canSetState(key, true)) { if (mKeyProvider.getPosition(key) == RecyclerView.NO_POSITION
|| !canSetState(key, true)) {
if (toRemove == null) { if (toRemove == null) {
toRemove = new ArrayList<>(); toRemove = new ArrayList<>();
} }
@ -420,10 +427,13 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> implements R
if (toRemove != null) { if (toRemove != null) {
for (K key : toRemove) { for (K key : toRemove) {
// TODO(b/163840879): Calling deselect fires onSelectionChanged
// once per call. Meaning we're firing it n+1 times when deselecting.
deselect(key); deselect(key);
} }
} }
// TODO: Send onSelectionCleared if empty in 2.0 release.
notifySelectionChanged(); notifySelectionChanged();
} }
@ -552,7 +562,7 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> implements R
return; return;
} }
@Nullable Bundle selectionState = state.getBundle(getInstanceStateKey()); Bundle selectionState = state.getBundle(getInstanceStateKey());
if (selectionState == null) { if (selectionState == null) {
return; return;
} }
@ -613,6 +623,10 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> implements R
public void onItemRangeRemoved(int startPosition, int itemCount) { public void onItemRangeRemoved(int startPosition, int itemCount) {
if (mSelectionTracker.isOverlapping(startPosition, itemCount)) if (mSelectionTracker.isOverlapping(startPosition, itemCount))
mSelectionTracker.endRange(); mSelectionTracker.endRange();
// Since SelectionTracker deals in keys, not positions, we turn
// to the `onDataSetChanged` sledge hammer.
// DefaultSelectionTracker will validate and update it's selection.
mSelectionTracker.onDataSetChanged();
} }
@Override @Override

@ -0,0 +1,72 @@
/*
* Copyright 2020 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.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
import org.jspecify.annotations.NonNull;
/**
* Wrapper class that regulates delivery of MotionEvents to delegate listeners, uniformly
* honoring requests to onRequestDisallowInterceptTouchEvent.
* Wrap this class around other OnItemTouchListeners to bestow them with
* proper support for onRequestDisallowInterceptTouchEvent.
*/
// TODO: Replace in-situ "disallow" implementation in EventRouter, ResetManager,
// BandSelectionHelper, GestureSelectionHelper by wrapping w/ this class.
class DisallowInterceptFilter implements
OnItemTouchListener, Resettable {
private final OnItemTouchListener mDelegate;
private boolean mDisallowIntercept;
DisallowInterceptFilter(@NonNull OnItemTouchListener delegate) {
mDelegate = delegate;
}
@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
// Reset disallow when the event is down as advised in http://b/139141511#comment20.
if (mDisallowIntercept && MotionEvents.isActionDown(e)) {
mDisallowIntercept = false;
}
return !mDisallowIntercept && mDelegate.onInterceptTouchEvent(rv, e);
}
@Override
public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
mDelegate.onInterceptTouchEvent(rv, e);
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
mDisallowIntercept = true;
}
@Override
public boolean isResetRequired() {
return mDisallowIntercept;
}
@Override
public void reset() {
mDisallowIntercept = false;
}
}

@ -0,0 +1,76 @@
/*
* Copyright 2020 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.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
import org.jspecify.annotations.NonNull;
/**
* OnItemTouchListener that claims all ACTION_UP events in streams that have otherwise gone
* unclaimed after a LongPress has been detected by GestureDetector.
* This addresses issue described in b/166836317, where child view
* OnClickListeners were being called unexpectedly.
*/
class EventBackstop implements OnItemTouchListener, Resettable {
private boolean mLongPressFired;
@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
// We only claim ACTION_UP events after a LongPress event. Were we to claim
// all ACTION_UP events we'd deprive RecyclerView of the signal it needs to
// initiate fling scrolling.
if (MotionEvents.isActionUp(e) && mLongPressFired) {
mLongPressFired = false;
return true;
}
// Recover from disallow state.
if (MotionEvents.isActionDown(e) && isResetRequired()) {
reset();
}
return false;
}
@Override
public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
// We should never receive any events, but were we to...we want to ignore them.
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
throw new UnsupportedOperationException("Wrap me in an InterceptFilter.");
}
@Override
public boolean isResetRequired() {
return mLongPressFired;
}
@Override
public void reset() {
mLongPressFired = false;
}
void onLongPress() {
mLongPressFired = true;
}
}

@ -17,17 +17,17 @@
package androidx.recyclerview.selection; package androidx.recyclerview.selection;
import static androidx.annotation.RestrictTo.Scope.LIBRARY; import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE;
import static androidx.core.util.Preconditions.checkArgument; import static androidx.core.util.Preconditions.checkArgument;
import static androidx.recyclerview.selection.Shared.VERBOSE; import static androidx.recyclerview.selection.Shared.VERBOSE;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting; import androidx.core.util.Consumer;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import org.jspecify.annotations.NonNull;
/** /**
* Provides the necessary glue to notify RecyclerView when selection data changes, * Provides the necessary glue to notify RecyclerView when selection data changes,
* and to notify SelectionTracker when the underlying RecyclerView.Adapter data changes. * and to notify SelectionTracker when the underlying RecyclerView.Adapter data changes.
@ -36,10 +36,8 @@ import androidx.recyclerview.widget.RecyclerView;
* with multiple RecyclerView instances. This may be necessary when multiple * with multiple RecyclerView instances. This may be necessary when multiple
* different views of data are presented to the user. * different views of data are presented to the user.
* *
* @hide
*/ */
@RestrictTo(LIBRARY) @RestrictTo(LIBRARY)
@VisibleForTesting(otherwise = PACKAGE_PRIVATE)
public class EventBridge { public class EventBridge {
private static final String TAG = "EventsRelays"; private static final String TAG = "EventsRelays";
@ -50,37 +48,45 @@ public class EventBridge {
* @param adapter * @param adapter
* @param selectionTracker * @param selectionTracker
* @param keyProvider * @param keyProvider
* @param runner Callback allowing operation to be run at next opportune time.
* Implementation could be {@link RecyclerView#postOnAnimation(Runnable)}.
* *
* @param <K> Selection key type. @see {@link StorageStrategy} for supported types. * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
*/ */
public static <K> void install( public static <K> void install(
@NonNull RecyclerView.Adapter<?> adapter, RecyclerView.@NonNull Adapter<?> adapter,
@NonNull SelectionTracker<K> selectionTracker, @NonNull SelectionTracker<K> selectionTracker,
@NonNull ItemKeyProvider<K> keyProvider) { @NonNull ItemKeyProvider<K> keyProvider,
@NonNull Consumer<Runnable> runner) {
// setup bridges to relay selection and adapter events // setup bridges to relay selection and adapter events
new TrackerToAdapterBridge<>(selectionTracker, keyProvider, adapter); new TrackerToAdapterBridge<>(selectionTracker, keyProvider, adapter, runner);
adapter.registerAdapterDataObserver(selectionTracker.getAdapterDataObserver()); adapter.registerAdapterDataObserver(selectionTracker.getAdapterDataObserver());
} }
private static final class TrackerToAdapterBridge<K> private static final class TrackerToAdapterBridge<K>
extends SelectionTracker.SelectionObserver<K> { extends SelectionTracker.SelectionObserver<K> {
// Non-private as necessary to avoid synthetic accessors for inner classes.
final RecyclerView.Adapter<?> mAdapter;
private final ItemKeyProvider<K> mKeyProvider; private final ItemKeyProvider<K> mKeyProvider;
private final RecyclerView.Adapter<?> mAdapter; private final Consumer<Runnable> mRunner;
TrackerToAdapterBridge( TrackerToAdapterBridge(
@NonNull SelectionTracker<K> selectionTracker, @NonNull SelectionTracker<K> selectionTracker,
@NonNull ItemKeyProvider<K> keyProvider, @NonNull ItemKeyProvider<K> keyProvider,
@NonNull RecyclerView.Adapter<?> adapter) { RecyclerView.@NonNull Adapter<?> adapter,
Consumer<Runnable> runner) {
selectionTracker.addObserver(this); selectionTracker.addObserver(this);
checkArgument(keyProvider != null); checkArgument(keyProvider != null);
checkArgument(adapter != null); checkArgument(adapter != null);
checkArgument(runner != null);
mKeyProvider = keyProvider; mKeyProvider = keyProvider;
mAdapter = adapter; mAdapter = adapter;
mRunner = runner;
} }
/** /**
@ -96,7 +102,12 @@ public class EventBridge {
return; return;
} }
mAdapter.notifyItemChanged(position, SelectionTracker.SELECTION_CHANGED_MARKER); mRunner.accept(new Runnable() {
@Override
public void run() {
mAdapter.notifyItemChanged(position, SelectionTracker.SELECTION_CHANGED_MARKER);
}
});
} }
} }

@ -20,10 +20,11 @@ import static androidx.core.util.Preconditions.checkArgument;
import android.view.MotionEvent; import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener; import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
import org.jspecify.annotations.NonNull;
/** /**
* A class responsible for routing MotionEvents to tool-type specific handlers. * A class responsible for routing MotionEvents to tool-type specific handlers.
* Individual tool-type specific handlers are added after the class is constructed. * Individual tool-type specific handlers are added after the class is constructed.
@ -33,37 +34,62 @@ import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
* {@link RecyclerView#addOnItemTouchListener(OnItemTouchListener)}. Despite "Touch" * {@link RecyclerView#addOnItemTouchListener(OnItemTouchListener)}. Despite "Touch"
* being in the name, it receives MotionEvents for all types of tools. * being in the name, it receives MotionEvents for all types of tools.
*/ */
final class EventRouter implements OnItemTouchListener { final class EventRouter implements OnItemTouchListener, Resettable {
private final ToolHandlerRegistry<OnItemTouchListener> mDelegates; private final ToolSourceHandlerRegistry<OnItemTouchListener> mDelegates;
private boolean mDisallowIntercept;
EventRouter() { EventRouter() {
mDelegates = new ToolHandlerRegistry<>(new DummyOnItemTouchListener()); mDelegates = new ToolSourceHandlerRegistry<>(new StubOnItemTouchListener());
} }
/** /**
* @param toolType See MotionEvent for details on available types. * @param key Either a TOOL_TYPE or a combination of TOOL_TYPE and SOURCE
* @param delegate An {@link OnItemTouchListener} to receive events * @param delegate An {@link OnItemTouchListener} to receive events of {@code ToolSourceKey}.
* of {@code toolType}.
*/ */
void set(int toolType, @NonNull OnItemTouchListener delegate) { void set(@NonNull ToolSourceKey key, @NonNull OnItemTouchListener delegate) {
checkArgument(delegate != null); checkArgument(delegate != null);
mDelegates.set(toolType, delegate); mDelegates.set(key, delegate);
} }
@Override @Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
return mDelegates.get(e).onInterceptTouchEvent(rv, e); // Reset disallow when the event is down as advised in http://b/139141511#comment20.
if (mDisallowIntercept && MotionEvents.isActionDown(e)) {
mDisallowIntercept = false;
}
return !mDisallowIntercept && mDelegates.get(e).onInterceptTouchEvent(rv, e);
} }
@Override @Override
public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
mDelegates.get(e).onTouchEvent(rv, e); if (!mDisallowIntercept) {
mDelegates.get(e).onTouchEvent(rv, e);
}
} }
@Override @Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
// TODO(b/139141511): Handle onRequestDisallowInterceptTouchEvent. if (!disallowIntercept) {
return; // Ignore as advised in http://b/139141511#comment20
}
// Some types of views, such as HorizontalScrollView, may want
// to take over the input stream. In this case they'll call this method
// with disallowIntercept=true. mDisallowIntercept is reset on UP or CANCEL
// events in onInterceptTouchEvent.
mDisallowIntercept = disallowIntercept;
}
@Override
public boolean isResetRequired() {
return mDisallowIntercept;
}
@Override
public void reset() {
mDisallowIntercept = false;
} }
} }

@ -16,10 +16,11 @@
package androidx.recyclerview.selection; package androidx.recyclerview.selection;
import androidx.annotation.NonNull;
import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails; import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import org.jspecify.annotations.NonNull;
/** /**
* Override methods in this class to provide application specific behaviors * Override methods in this class to provide application specific behaviors
* related to focusing item. * related to focusing item.
@ -28,7 +29,7 @@ import androidx.recyclerview.widget.RecyclerView;
*/ */
public abstract class FocusDelegate<K> { public abstract class FocusDelegate<K> {
static <K> FocusDelegate<K> dummy() { static <K> FocusDelegate<K> stub() {
return new FocusDelegate<K>() { return new FocusDelegate<K>() {
@Override @Override
public void focusItem(@NonNull ItemDetails<K> item) { public void focusItem(@NonNull ItemDetails<K> item) {

@ -1,58 +0,0 @@
/*
* 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) {
}
}

@ -0,0 +1,99 @@
/*
* 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.recyclerview.widget.RecyclerView;
import org.jspecify.annotations.NonNull;
/**
* A wrapper class for GestureDetector allowing it interact with SelectionTracker
* and its dependencies (like RecyclerView) on terms more amenable to SelectionTracker.
*/
final class GestureDetectorWrapper implements RecyclerView.OnItemTouchListener, Resettable {
private final GestureDetector mDetector;
private boolean mDisallowIntercept;
GestureDetectorWrapper(@NonNull GestureDetector detector) {
checkArgument(detector != null);
mDetector = detector;
}
@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
// Reset disallow when the event is down as advised in http://b/139141511#comment20.
if (mDisallowIntercept && MotionEvents.isActionDown(e)) {
mDisallowIntercept = false;
}
// 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 !mDisallowIntercept && mDetector.onTouchEvent(e);
}
@Override
public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (!disallowIntercept) {
return; // Ignore as advised in http://b/139141511#comment20
}
// Some types of views, such as HorizontalScrollView, may want
// to take over the input stream. In this case they'll call this method
// with disallowIntercept=true. mDisallowIntercept is reset on UP or CANCEL
// events in onInterceptTouchEvent.
mDisallowIntercept = disallowIntercept;
// GestureDetector may have internal state (such as timers) that can
// result in subsequent event handlers being called, even after
// we receive a request to disallow intercept (e.g. LONG_PRESS).
// For that reason we proactively reset GestureDetector.
sendCancelEvent();
}
@Override
public boolean isResetRequired() {
// Always resettable as we don't know the specifics of GD's internal state.
return true;
}
@Override
public void reset() {
mDisallowIntercept = false;
sendCancelEvent();
}
private void sendCancelEvent() {
// GestureDetector does not provide a public affordance for resetting
// it's internal state, so we send it a synthetic ACTION_CANCEL event.
mDetector.onTouchEvent(MotionEvents.createCancelEvent());
}
}

@ -23,8 +23,8 @@ import android.view.GestureDetector.OnGestureListener;
import android.view.GestureDetector.SimpleOnGestureListener; import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.MotionEvent; import android.view.MotionEvent;
import androidx.annotation.NonNull; import org.jspecify.annotations.NonNull;
import androidx.annotation.Nullable; import org.jspecify.annotations.Nullable;
/** /**
* GestureRouter is responsible for routing gestures detected by a GestureDetector * GestureRouter is responsible for routing gestures detected by a GestureDetector
@ -36,11 +36,11 @@ import androidx.annotation.Nullable;
final class GestureRouter<T extends OnGestureListener & OnDoubleTapListener> final class GestureRouter<T extends OnGestureListener & OnDoubleTapListener>
implements OnGestureListener, OnDoubleTapListener { implements OnGestureListener, OnDoubleTapListener {
private final ToolHandlerRegistry<T> mDelegates; private final ToolSourceHandlerRegistry<T> mDelegates;
GestureRouter(@NonNull T defaultDelegate) { GestureRouter(@NonNull T defaultDelegate) {
checkArgument(defaultDelegate != null); checkArgument(defaultDelegate != null);
mDelegates = new ToolHandlerRegistry<>(defaultDelegate); mDelegates = new ToolSourceHandlerRegistry<>(defaultDelegate);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@ -49,11 +49,11 @@ final class GestureRouter<T extends OnGestureListener & OnDoubleTapListener>
} }
/** /**
* @param toolType * @param key
* @param delegate the delegate, or null to unregister. * @param delegate the delegate, or null to unregister.
*/ */
public void register(int toolType, @Nullable T delegate) { public void register(@NonNull ToolSourceKey key, @Nullable T delegate) {
mDelegates.set(toolType, delegate); mDelegates.set(key, delegate);
} }
@Override @Override

@ -24,13 +24,13 @@ import android.util.Log;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate; import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener; import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
import org.jspecify.annotations.NonNull;
/** /**
* GestureSelectionHelper provides logic that interprets a combination * GestureSelectionHelper provides logic that interprets a combination
* of motions and gestures in order to provide gesture driven selection support * of motions and gestures in order to provide gesture driven selection support
@ -96,7 +96,6 @@ final class GestureSelectionHelper implements OnItemTouchListener, Resettable {
} }
@Override @Override
/** @hide */
public boolean onInterceptTouchEvent(@NonNull RecyclerView unused, @NonNull MotionEvent e) { public boolean onInterceptTouchEvent(@NonNull RecyclerView unused, @NonNull MotionEvent e) {
// MotionEvents that aren't ACTION_DOWN are only ever passed to either onInterceptTouchEvent // 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 // or onTouchEvent; never to both, so events delivered to this method are effectively
@ -120,7 +119,6 @@ final class GestureSelectionHelper implements OnItemTouchListener, Resettable {
} }
@Override @Override
/** @hide */
public void onTouchEvent(@NonNull RecyclerView unused, @NonNull MotionEvent e) { public void onTouchEvent(@NonNull RecyclerView unused, @NonNull MotionEvent e) {
if (!mStarted) { if (!mStarted) {
if (VERBOSE) Log.i(TAG, "Ignoring input event. Not started."); if (VERBOSE) Log.i(TAG, "Ignoring input event. Not started.");
@ -147,7 +145,6 @@ final class GestureSelectionHelper implements OnItemTouchListener, Resettable {
} }
@Override @Override
/** @hide */
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
} }
@ -268,11 +265,12 @@ final class GestureSelectionHelper implements OnItemTouchListener, Resettable {
@Override @Override
int getLastGlidedItemPosition(@NonNull MotionEvent e) { int getLastGlidedItemPosition(@NonNull MotionEvent e) {
// If user has moved his pointer to the bottom-right empty pane (ie. to the right of the // If user has moved their pointer to the bottom-right empty pane (ie. to the
// last item of the recycler view), we would want to set that as the currentItemPos // right of the last item of the recycler view), we would want to set that as
// the currentItemPos
View lastItem = mRecyclerView.getLayoutManager() View lastItem = mRecyclerView.getLayoutManager()
.getChildAt(mRecyclerView.getLayoutManager().getChildCount() - 1); .getChildAt(mRecyclerView.getLayoutManager().getChildCount() - 1);
int direction = ViewCompat.getLayoutDirection(mRecyclerView); int direction = mRecyclerView.getLayoutDirection();
final boolean pastLastItem = isPastLastItem(lastItem.getTop(), final boolean pastLastItem = isPastLastItem(lastItem.getTop(),
lastItem.getLeft(), lastItem.getLeft(),
lastItem.getRight(), lastItem.getRight(),

@ -17,6 +17,7 @@
package androidx.recyclerview.selection; package androidx.recyclerview.selection;
import static androidx.core.util.Preconditions.checkArgument; import static androidx.core.util.Preconditions.checkArgument;
import static androidx.core.util.Preconditions.checkState;
import android.graphics.Point; import android.graphics.Point;
import android.graphics.Rect; import android.graphics.Rect;
@ -25,12 +26,13 @@ import android.util.SparseArray;
import android.util.SparseBooleanArray; import android.util.SparseBooleanArray;
import android.util.SparseIntArray; import android.util.SparseIntArray;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate; import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.OnScrollListener; import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
@ -145,6 +147,7 @@ final class GridModel<K> {
mIsActive = true; mIsActive = true;
mPointer = mHost.createAbsolutePoint(relativeOrigin); mPointer = mHost.createAbsolutePoint(relativeOrigin);
mRelOrigin = createRelativePoint(mPointer); mRelOrigin = createRelativePoint(mPointer);
mRelPointer = createRelativePoint(mPointer); mRelPointer = createRelativePoint(mPointer);
computeCurrentSelection(); computeCurrentSelection();
@ -171,7 +174,11 @@ final class GridModel<K> {
*/ */
void resizeSelection(Point relativePointer) { void resizeSelection(Point relativePointer) {
mPointer = mHost.createAbsolutePoint(relativePointer); mPointer = mHost.createAbsolutePoint(relativePointer);
updateModel(); // Should probably never been empty at this point, yet we guard against
// known exceptions because wholesome goodness.
if (!isEmpty()) {
updateModel();
}
} }
/** /**
@ -191,7 +198,12 @@ final class GridModel<K> {
mPointer.x += dx; mPointer.x += dx;
mPointer.y += dy; mPointer.y += dy;
recordVisibleChildren(); recordVisibleChildren();
updateModel();
// Should probably never been empty at this point, yet we guard against
// known exceptions because wholesome goodness.
if (!isEmpty()) {
updateModel();
}
} }
/** /**
@ -258,7 +270,9 @@ final class GridModel<K> {
* in a selection change and, if it has, notifies listeners of this change. * in a selection change and, if it has, notifies listeners of this change.
*/ */
private void updateModel() { private void updateModel() {
checkState(!isEmpty());
RelativePoint old = mRelPointer; RelativePoint old = mRelPointer;
mRelPointer = createRelativePoint(mPointer); mRelPointer = createRelativePoint(mPointer);
if (mRelPointer.equals(old)) { if (mRelPointer.equals(old)) {
return; return;
@ -590,6 +604,11 @@ final class GridModel<K> {
} }
RelativePoint createRelativePoint(Point point) { RelativePoint createRelativePoint(Point point) {
// mColumnBounds and mRowBounds is empty when there are no items in the view.
// Clients have to verify items exist before calling this method.
checkState(!mColumnBounds.isEmpty(), "Column bounds not established.");
checkState(!mRowBounds.isEmpty(), "Row bounds not established.");
return new RelativePoint( return new RelativePoint(
new RelativeCoordinate(mColumnBounds, point.x), new RelativeCoordinate(mColumnBounds, point.x),
new RelativeCoordinate(mRowBounds, point.y)); new RelativeCoordinate(mRowBounds, point.y));
@ -604,14 +623,6 @@ final class GridModel<K> {
final RelativeCoordinate mX; final RelativeCoordinate mX;
final RelativeCoordinate mY; final RelativeCoordinate mY;
RelativePoint(
@NonNull List<Limits> columnLimits,
@NonNull List<Limits> rowLimits, Point point) {
this.mX = new RelativeCoordinate(columnLimits, point.x);
this.mY = new RelativeCoordinate(rowLimits, point.y);
}
RelativePoint(@NonNull RelativeCoordinate x, @NonNull RelativeCoordinate y) { RelativePoint(@NonNull RelativeCoordinate x, @NonNull RelativeCoordinate y) {
this.mX = x; this.mX = x;
this.mY = y; this.mY = y;

@ -20,11 +20,12 @@ import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import android.view.MotionEvent; import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
/** /**
* The Selection library calls {@link #getItemDetails(MotionEvent)} when it needs * The Selection library calls {@link #getItemDetails(MotionEvent)} when it needs
* access to information about the area and/or {@link ItemDetails} under a {@link MotionEvent}. * access to information about the area and/or {@link ItemDetails} under a {@link MotionEvent}.
@ -70,7 +71,6 @@ public abstract class ItemDetailsLookup<K> {
/** /**
* @return true if there is an item w/ a stable ID at the event coordinates. * @return true if there is an item w/ a stable ID at the event coordinates.
* @hide
*/ */
@RestrictTo(LIBRARY) @RestrictTo(LIBRARY)
protected boolean overItemWithSelectionKey(@NonNull MotionEvent e) { protected boolean overItemWithSelectionKey(@NonNull MotionEvent e) {
@ -100,7 +100,7 @@ public abstract class ItemDetailsLookup<K> {
* @return the adapter position of the item at the event coordinates. * @return the adapter position of the item at the event coordinates.
*/ */
final int getItemPosition(@NonNull MotionEvent e) { final int getItemPosition(@NonNull MotionEvent e) {
@Nullable ItemDetails<?> item = getItemDetails(e); ItemDetails<?> item = getItemDetails(e);
return item != null return item != null
? item.getPosition() ? item.getPosition()
: RecyclerView.NO_POSITION; : RecyclerView.NO_POSITION;
@ -172,7 +172,8 @@ public abstract class ItemDetailsLookup<K> {
/** /**
* Returns the adapter position of the item. See * Returns the adapter position of the item. See
* {@link RecyclerView.ViewHolder#getAdapterPosition() ViewHolder.getAdapterPosition} * {@link RecyclerView.ViewHolder#getAbsoluteAdapterPosition() ViewHolder
* .getAbsoluteAdapterPosition}
* *
* @return the position of an item. * @return the position of an item.
*/ */

@ -19,10 +19,11 @@ package androidx.recyclerview.selection;
import static androidx.core.util.Preconditions.checkArgument; import static androidx.core.util.Preconditions.checkArgument;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
@ -79,7 +80,8 @@ public abstract class ItemKeyProvider<K> {
public abstract @Nullable K getKey(int position); public abstract @Nullable K getKey(int position);
/** /**
* @return the position corresponding to the selection key, or RecyclerView.NO_POSITION. * @return the position corresponding to the selection key, or RecyclerView.NO_POSITION
* if the key is unrecognized.
*/ */
public abstract int getPosition(@NonNull K key); public abstract int getPosition(@NonNull K key);
} }

@ -17,17 +17,27 @@
package androidx.recyclerview.selection; package androidx.recyclerview.selection;
import android.graphics.Point; import android.graphics.Point;
import android.view.InputDevice;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.MotionEvent; import android.view.MotionEvent;
import androidx.annotation.NonNull; import org.jspecify.annotations.NonNull;
/** /**
* Utility methods for working with {@link MotionEvent} instances. * Utility methods for working with {@link MotionEvent} instances.
*/ */
final class MotionEvents { final class MotionEvents {
private MotionEvents() {} private MotionEvents() {
}
static boolean isTouchpadEvent(@NonNull MotionEvent e) {
// ChromeOS ARC devices with touchpads emit their events with
// {@link MotionEvent#TOOL_TYPE_MOUSE}, so this is specifically capturing non-ARC devices
// with touchpads (e.g. attachable keyboards with touchpads on Android tablets).
return e.getToolType(0) == MotionEvent.TOOL_TYPE_FINGER
&& e.getSource() == InputDevice.SOURCE_MOUSE;
}
static boolean isMouseEvent(@NonNull MotionEvent e) { static boolean isMouseEvent(@NonNull MotionEvent e) {
return e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE; return e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE;
@ -101,20 +111,22 @@ final class MotionEvents {
static boolean isTouchpadScroll(@NonNull MotionEvent e) { static boolean isTouchpadScroll(@NonNull MotionEvent e) {
// Touchpad inputs are treated as mouse inputs, and when scrolling, there are no buttons // Touchpad inputs are treated as mouse inputs, and when scrolling, there are no buttons
// returned. // returned.
return isMouseEvent(e) && isActionMove(e) && e.getButtonState() == 0; return (isTouchpadEvent(e) || isMouseEvent(e)) && isActionMove(e)
} && e.getButtonState() == 0;
/**
* Returns true if the event is a drag event (which is presumbaly, but not
* explicitly required to be a mouse event).
* @param e
*/
static boolean isPointerDragEvent(MotionEvent e) {
return isPrimaryMouseButtonPressed(e)
&& isActionMove(e);
} }
private static boolean hasBit(int metaState, int bit) { private static boolean hasBit(int metaState, int bit) {
return (metaState & bit) != 0; return (metaState & bit) != 0;
} }
static MotionEvent createCancelEvent() {
return MotionEvent.obtain(
0, // down time
1, // event time
MotionEvent.ACTION_CANCEL,
0, // x
0, // y
0 // metaState
);
}
} }

@ -22,11 +22,12 @@ import static androidx.core.util.Preconditions.checkState;
import android.view.GestureDetector.SimpleOnGestureListener; import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.MotionEvent; import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails; import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
/** /**
* Base class for handlers that can be registered w/ {@link GestureRouter}. * Base class for handlers that can be registered w/ {@link GestureRouter}.
*/ */

@ -23,11 +23,11 @@ import static androidx.recyclerview.selection.Shared.VERBOSE;
import android.util.Log; import android.util.Log;
import android.view.MotionEvent; import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails; import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import org.jspecify.annotations.NonNull;
/** /**
* A MotionInputHandler that provides the high-level glue for mouse driven selection. This * A MotionInputHandler that provides the high-level glue for mouse driven selection. This
* class works with {@link RecyclerView}, {@link GestureRouter}, and {@link GestureSelectionHelper} * class works with {@link RecyclerView}, {@link GestureRouter}, and {@link GestureSelectionHelper}
@ -35,7 +35,7 @@ import androidx.recyclerview.widget.RecyclerView;
*/ */
final class MouseInputHandler<K> extends MotionInputHandler<K> { final class MouseInputHandler<K> extends MotionInputHandler<K> {
private static final String TAG = "MouseInputDelegate"; private static final String TAG = "MouseInputHandler";
private final ItemDetailsLookup<K> mDetailsLookup; private final ItemDetailsLookup<K> mDetailsLookup;
private final OnContextClickListener mOnContextClickListener; private final OnContextClickListener mOnContextClickListener;
@ -170,7 +170,7 @@ final class MouseInputHandler<K> extends MotionInputHandler<K> {
return false; return false;
} }
@Nullable ItemDetails<K> item = mDetailsLookup.getItemDetails(e); ItemDetails<K> item = mDetailsLookup.getItemDetails(e);
if (item == null || !item.hasSelectionKey()) { if (item == null || !item.hasSelectionKey()) {
return false; return false;
} }
@ -204,7 +204,7 @@ final class MouseInputHandler<K> extends MotionInputHandler<K> {
private boolean onRightClick(@NonNull MotionEvent e) { private boolean onRightClick(@NonNull MotionEvent e) {
if (mDetailsLookup.overItemWithSelectionKey(e)) { if (mDetailsLookup.overItemWithSelectionKey(e)) {
@Nullable ItemDetails<K> item = mDetailsLookup.getItemDetails(e); ItemDetails<K> item = mDetailsLookup.getItemDetails(e);
if (item != null && !mSelectionTracker.isSelected(item.getSelectionKey())) { if (item != null && !mSelectionTracker.isSelected(item.getSelectionKey())) {
mSelectionTracker.clearSelection(); mSelectionTracker.clearSelection();
selectItem(item); selectItem(item);

@ -16,7 +16,7 @@
package androidx.recyclerview.selection; package androidx.recyclerview.selection;
import androidx.annotation.NonNull; import org.jspecify.annotations.NonNull;
/** /**
* Subclass of {@link Selection} exposing public support for mutating the underlying * Subclass of {@link Selection} exposing public support for mutating the underlying

@ -18,7 +18,7 @@ package androidx.recyclerview.selection;
import android.view.MotionEvent; import android.view.MotionEvent;
import androidx.annotation.NonNull; import org.jspecify.annotations.NonNull;
/** /**
* Override methods in this class to provide application specific behaviors * Override methods in this class to provide application specific behaviors

@ -22,7 +22,7 @@ import android.content.ClipData;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import androidx.annotation.NonNull; import org.jspecify.annotations.NonNull;
/** /**
* Register an OnDragInitiatedListener to be notified when user intent to perform drag and drop * Register an OnDragInitiatedListener to be notified when user intent to perform drag and drop

@ -18,9 +18,10 @@ package androidx.recyclerview.selection;
import android.view.MotionEvent; import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails; import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
import org.jspecify.annotations.NonNull;
/** /**
* Register an OnItemActivatedListener to be notified when an item is activated * Register an OnItemActivatedListener to be notified when an item is activated
* (tapped or double clicked). * (tapped or double clicked).
@ -31,7 +32,7 @@ public interface OnItemActivatedListener<K> {
/** /**
* Called when an item is "activated". An item is activated, for example, when no selection * Called when an item is "activated". An item is activated, for example, when no selection
* exists and the user taps an item with her finger, or double clicks an item with a * exists and the user taps an item with their finger, or double clicks an item with a
* pointing device like a Mouse. * pointing device like a Mouse.
* *
* @param item details of the item. * @param item details of the item.

@ -24,9 +24,10 @@ import static androidx.recyclerview.selection.Shared.DEBUG;
import android.util.Log; import android.util.Log;
import androidx.annotation.MainThread; import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo;
import org.jspecify.annotations.NonNull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -52,7 +53,7 @@ public final class OperationMonitor {
// Ideally OperationMonitor would implement Resettable // Ideally OperationMonitor would implement Resettable
// directly, but Metalava couldn't understand that // directly, but Metalava couldn't understand that
// `OperationMonitor` was public API while `Resettable` was // `OperationMonitor` was public API while `Resettable` was
// not. This is our klunkuy workaround. // not. This is our clever workaround :)
private final Resettable mResettable = new Resettable() { private final Resettable mResettable = new Resettable() {
@Override @Override
@ -68,55 +69,65 @@ public final class OperationMonitor {
private int mNumOps = 0; private int mNumOps = 0;
private final Object mLock = new Object();
@MainThread @MainThread
synchronized void start() { void start() {
mNumOps++; synchronized (mLock) {
mNumOps++;
if (mNumOps == 1) { if (mNumOps == 1) {
notifyStateChanged(); notifyStateChanged();
} }
if (DEBUG) Log.v(TAG, "Incremented content lock count to " + mNumOps + "."); if (DEBUG) Log.v(TAG, "Incremented content lock count to " + mNumOps + ".");
}
} }
@MainThread @MainThread
synchronized void stop() { void stop() {
if (mNumOps == 0) { synchronized (mLock) {
if (DEBUG) Log.w(TAG, "Stop called whith opt count of 0."); if (mNumOps == 0) {
return; 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 + "."); mNumOps--;
if (DEBUG) Log.v(TAG, "Decremented content lock count to " + mNumOps + ".");
if (mNumOps == 0) {
notifyStateChanged(); if (mNumOps == 0) {
notifyStateChanged();
}
} }
} }
/** @hide */
@RestrictTo(LIBRARY) @RestrictTo(LIBRARY)
@MainThread @MainThread
synchronized void reset() { void reset() {
if (DEBUG) Log.d(TAG, "Received reset request."); synchronized (mLock) {
if (mNumOps > 0) { if (DEBUG) Log.d(TAG, "Received reset request.");
Log.w(TAG, "Resetting OperationMonitor with " + mNumOps + " active operations."); if (mNumOps > 0) {
Log.w(TAG, "Resetting OperationMonitor with " + mNumOps + " active operations.");
}
mNumOps = 0;
notifyStateChanged();
} }
mNumOps = 0;
notifyStateChanged();
} }
/** @hide */
@RestrictTo(LIBRARY) @RestrictTo(LIBRARY)
synchronized boolean isResetRequired() { boolean isResetRequired() {
return isStarted(); synchronized (mLock) {
return isStarted();
}
} }
/** /**
* @return true if there are any running operations. * @return true if there are any running operations.
*/ */
public synchronized boolean isStarted() { public boolean isStarted() {
return mNumOps > 0; synchronized (mLock) {
return mNumOps > 0;
}
} }
/** /**
@ -154,7 +165,6 @@ public final class OperationMonitor {
/** /**
* Work around b/139109223. * Work around b/139109223.
* @hide
*/ */
@RestrictTo(LIBRARY) @RestrictTo(LIBRARY)
@NonNull Resettable asResettable() { @NonNull Resettable asResettable() {

@ -19,14 +19,16 @@ package androidx.recyclerview.selection;
import static androidx.core.util.Preconditions.checkArgument; import static androidx.core.util.Preconditions.checkArgument;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.ViewConfiguration;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener; import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
import org.jspecify.annotations.Nullable;
/** /**
* OnItemTouchListener that detects and delegates drag events to a drag listener, * OnItemTouchListener that detects and delegates drag events to a drag listener, else sends event
* else sends event to fallback {@link OnItemTouchListener}. * to fallback {@link OnItemTouchListener}.
* *
* <p>See {@link OnDragInitiatedListener} for details on implementing drag and drop. * <p>See {@link OnDragInitiatedListener} for details on implementing drag and drop.
*/ */
@ -35,6 +37,9 @@ final class PointerDragEventInterceptor implements OnItemTouchListener {
private final ItemDetailsLookup<?> mEventDetailsLookup; private final ItemDetailsLookup<?> mEventDetailsLookup;
private final OnDragInitiatedListener mDragListener; private final OnDragInitiatedListener mDragListener;
private OnItemTouchListener mDelegate; private OnItemTouchListener mDelegate;
private float mDownX;
private float mDownY;
private boolean mDownInItemDragRegion;
PointerDragEventInterceptor( PointerDragEventInterceptor(
ItemDetailsLookup<?> eventDetailsLookup, ItemDetailsLookup<?> eventDetailsLookup,
@ -50,14 +55,28 @@ final class PointerDragEventInterceptor implements OnItemTouchListener {
if (delegate != null) { if (delegate != null) {
mDelegate = delegate; mDelegate = delegate;
} else { } else {
mDelegate = new DummyOnItemTouchListener(); mDelegate = new StubOnItemTouchListener();
} }
} }
@Override @Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
if (MotionEvents.isPointerDragEvent(e) && mEventDetailsLookup.inItemDragRegion(e)) { if (MotionEvents.isPrimaryMouseButtonPressed(e)) {
return mDragListener.onDragInitiated(e); float x = e.getX();
float y = e.getY();
if (MotionEvents.isActionDown(e)) {
mDownX = x;
mDownY = y;
mDownInItemDragRegion = mEventDetailsLookup.inItemDragRegion(e);
} else if (mDownInItemDragRegion && MotionEvents.isActionMove(e)) {
int touchSlop = ViewConfiguration.get(rv.getContext()).getScaledTouchSlop();
float dx = x - mDownX;
float dy = y - mDownY;
float distanceSquared = (dx * dx) + (dy * dy);
if (distanceSquared > (touchSlop * touchSlop)) {
return mDragListener.onDragInitiated(e);
}
}
} }
return mDelegate.onInterceptTouchEvent(rv, e); return mDelegate.onInterceptTouchEvent(rv, e);
} }

@ -23,7 +23,8 @@ import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
import android.util.Log; import android.util.Log;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import org.jspecify.annotations.NonNull;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;

@ -21,11 +21,12 @@ import static androidx.recyclerview.selection.Shared.DEBUG;
import android.util.Log; import android.util.Log;
import android.view.MotionEvent; import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.recyclerview.selection.SelectionTracker.SelectionObserver; import androidx.recyclerview.selection.SelectionTracker.SelectionObserver;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener; import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
import org.jspecify.annotations.NonNull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;

@ -28,7 +28,6 @@ import androidx.annotation.RestrictTo;
* should always return false when called immediately after {@link #reset()} * should always return false when called immediately after {@link #reset()}
* has been called. * has been called.
* *
* @hide
*/ */
@RestrictTo(LIBRARY) @RestrictTo(LIBRARY)
public interface Resettable { public interface Resettable {

@ -16,8 +16,8 @@
package androidx.recyclerview.selection; package androidx.recyclerview.selection;
import androidx.annotation.NonNull; import org.jspecify.annotations.NonNull;
import androidx.annotation.Nullable; import org.jspecify.annotations.Nullable;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;

@ -16,9 +16,10 @@
package androidx.recyclerview.selection; package androidx.recyclerview.selection;
import androidx.annotation.NonNull;
import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate; import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate;
import org.jspecify.annotations.NonNull;
/** /**
* Utility class for creating SelectionPredicate instances. Provides default * Utility class for creating SelectionPredicate instances. Provides default
* implementations for common cases like "single selection" and "select anything". * implementations for common cases like "single selection" and "select anything".
@ -34,7 +35,7 @@ public final class SelectionPredicates {
* @param <K> Selection key type. @see {@link StorageStrategy} for supported types. * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
* @return * @return
*/ */
public static @NonNull <K> SelectionPredicate<K> createSelectAnything() { public static <K> @NonNull SelectionPredicate<K> createSelectAnything() {
return new SelectionPredicate<K>() { return new SelectionPredicate<K>() {
@Override @Override
public boolean canSetStateForKey(@NonNull K key, boolean nextState) { public boolean canSetStateForKey(@NonNull K key, boolean nextState) {
@ -60,7 +61,7 @@ public final class SelectionPredicates {
* @param <K> Selection key type. @see {@link StorageStrategy} for supported types. * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
* @return * @return
*/ */
public static @NonNull <K> SelectionPredicate<K> createSelectSingleAnything() { public static <K> @NonNull SelectionPredicate<K> createSelectSingleAnything() {
return new SelectionPredicate<K>() { return new SelectionPredicate<K>() {
@Override @Override
public boolean canSetStateForKey(@NonNull K key, boolean nextState) { public boolean canSetStateForKey(@NonNull K key, boolean nextState) {

@ -19,22 +19,25 @@ package androidx.recyclerview.selection;
import static androidx.annotation.RestrictTo.Scope.LIBRARY; import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.core.util.Preconditions.checkArgument; import static androidx.core.util.Preconditions.checkArgument;
import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable; import android.os.Parcelable;
import android.util.Log; import android.util.Log;
import android.view.GestureDetector; import android.view.GestureDetector;
import android.view.HapticFeedbackConstants; import android.view.HapticFeedbackConstants;
import android.view.InputDevice;
import android.view.MotionEvent; import android.view.MotionEvent;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver; import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener; import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import java.util.Set; import java.util.Set;
/** /**
@ -90,8 +93,6 @@ import java.util.Set;
*/ */
public abstract class SelectionTracker<K> { public abstract class SelectionTracker<K> {
private static final String TAG = "SelectionTracker";
/** /**
* This value is included in the payload when SelectionTracker notifies RecyclerView * This value is included in the payload when SelectionTracker notifies RecyclerView
* of changes to selection. Look for this value in the {@code payload} * of changes to selection. Look for this value in the {@code payload}
@ -103,6 +104,7 @@ public abstract class SelectionTracker<K> {
* When state is being restored, this argument will not be present. * When state is being restored, this argument will not be present.
*/ */
public static final String SELECTION_CHANGED_MARKER = "Selection-Changed"; public static final String SELECTION_CHANGED_MARKER = "Selection-Changed";
private static final String TAG = "SelectionTracker";
/** /**
* Adds {@code observer} to be notified when changes to selection occur. * Adds {@code observer} to be notified when changes to selection occur.
@ -163,6 +165,7 @@ public abstract class SelectionTracker<K> {
* Sets the selected state of the specified items if permitted after consulting * Sets the selected state of the specified items if permitted after consulting
* SelectionPredicate. * SelectionPredicate.
*/ */
@SuppressLint("LambdaLast")
public abstract boolean setItemsSelected(@NonNull Iterable<K> keys, boolean selected); public abstract boolean setItemsSelected(@NonNull Iterable<K> keys, boolean selected);
/** /**
@ -181,7 +184,7 @@ public abstract class SelectionTracker<K> {
*/ */
public abstract boolean deselect(@NonNull K key); public abstract boolean deselect(@NonNull K key);
/** @hide */ @SuppressWarnings("HiddenAbstractMethod")
@RestrictTo(LIBRARY) @RestrictTo(LIBRARY)
protected abstract @NonNull AdapterDataObserver getAdapterDataObserver(); protected abstract @NonNull AdapterDataObserver getAdapterDataObserver();
@ -192,8 +195,8 @@ public abstract class SelectionTracker<K> {
* @param position The "anchor" position for the range. Subsequent range operations * @param position The "anchor" position for the range. Subsequent range operations
* (primarily keyboard and mouse based operations like SHIFT + click) * (primarily keyboard and mouse based operations like SHIFT + click)
* work with the established anchor point to define selection ranges. * work with the established anchor point to define selection ranges.
* @hide
*/ */
@SuppressWarnings("HiddenAbstractMethod")
@RestrictTo(LIBRARY) @RestrictTo(LIBRARY)
public abstract void startRange(int position); public abstract void startRange(int position);
@ -208,8 +211,8 @@ public abstract class SelectionTracker<K> {
* @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 * @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
*/ */
@SuppressWarnings("HiddenAbstractMethod")
@RestrictTo(LIBRARY) @RestrictTo(LIBRARY)
public abstract void extendRange(int position); public abstract void extendRange(int position);
@ -217,16 +220,15 @@ public abstract class SelectionTracker<K> {
* Clears an in-progress range selection. Provisional range selection established * Clears an in-progress range selection. Provisional range selection established
* using {@link #extendProvisionalRange(int)} will be cleared (unless * using {@link #extendProvisionalRange(int)} will be cleared (unless
* {@link #mergeProvisionalSelection()} is called first.) * {@link #mergeProvisionalSelection()} is called first.)
*
* @hide
*/ */
@SuppressWarnings("HiddenAbstractMethod")
@RestrictTo(LIBRARY) @RestrictTo(LIBRARY)
public abstract void endRange(); public abstract void endRange();
/** /**
* @return Whether or not there is a current range selection active. * @return Whether or not there is a current range selection active.
* @hide
*/ */
@SuppressWarnings("HiddenAbstractMethod")
@RestrictTo(LIBRARY) @RestrictTo(LIBRARY)
public abstract boolean isRangeActive(); public abstract boolean isRangeActive();
@ -238,8 +240,8 @@ public abstract class SelectionTracker<K> {
* TODO: Reconcile this with startRange. Maybe just docs need to be updated. * TODO: Reconcile this with startRange. Maybe just docs need to be updated.
* *
* @param position the anchor position. Must already be selected. * @param position the anchor position. Must already be selected.
* @hide
*/ */
@SuppressWarnings("HiddenAbstractMethod")
@RestrictTo(LIBRARY) @RestrictTo(LIBRARY)
public abstract void anchorRange(int position); public abstract void anchorRange(int position);
@ -247,33 +249,30 @@ public abstract class SelectionTracker<K> {
* Creates a provisional selection from anchor to {@code position}. * Creates a provisional selection from anchor to {@code position}.
* *
* @param position the end point. * @param position the end point.
* @hide
*/ */
@SuppressWarnings("HiddenAbstractMethod")
@RestrictTo(LIBRARY) @RestrictTo(LIBRARY)
protected abstract void extendProvisionalRange(int position); protected abstract void extendProvisionalRange(int position);
/** /**
* Sets the provisional selection, replacing any existing selection. * Sets the provisional selection, replacing any existing selection.
*
* @hide
*/ */
@SuppressWarnings("HiddenAbstractMethod")
@RestrictTo(LIBRARY) @RestrictTo(LIBRARY)
protected abstract void setProvisionalSelection(@NonNull Set<K> newSelection); protected abstract void setProvisionalSelection(@NonNull Set<K> newSelection);
/** /**
* Clears any existing provisional selection * Clears any existing provisional selection
*
* @hide
*/ */
@SuppressWarnings("HiddenAbstractMethod")
@RestrictTo(LIBRARY) @RestrictTo(LIBRARY)
protected abstract void clearProvisionalSelection(); protected abstract void clearProvisionalSelection();
/** /**
* Converts the provisional selection into primary selection, then clears * Converts the provisional selection into primary selection, then clears
* provisional selection. * provisional selection.
*
* @hide
*/ */
@SuppressWarnings("HiddenAbstractMethod")
@RestrictTo(LIBRARY) @RestrictTo(LIBRARY)
protected abstract void mergeProvisionalSelection(); protected abstract void mergeProvisionalSelection();
@ -308,8 +307,6 @@ public abstract class SelectionTracker<K> {
/** /**
* Called when Selection is cleared. * Called when Selection is cleared.
* TODO(smckay): Make public in a future public API. * TODO(smckay): Make public in a future public API.
*
* @hide
*/ */
@RestrictTo(LIBRARY) @RestrictTo(LIBRARY)
protected void onSelectionCleared() { protected void onSelectionCleared() {
@ -327,8 +324,7 @@ public abstract class SelectionTracker<K> {
/** /**
* Called immediately after completion of any set of changes, excluding * Called immediately after completion of any set of changes, excluding
* those resulting in calls to {@link #onSelectionRefresh()} and * those resulting in calls {@link #onSelectionRestored()}.
* {@link #onSelectionRestored()}.
*/ */
public void onSelectionChanged() { public void onSelectionChanged() {
} }
@ -385,59 +381,64 @@ public abstract class SelectionTracker<K> {
} }
/** /**
* Builder is the primary mechanism for create a {@link SelectionTracker} that * Builder is the primary mechanism for creating a {@link SelectionTracker} that
* can be used with your RecyclerView. Once installed, users will be able to create and * can be used with your RecyclerView. Once installed, users will be able to create and
* manipulate selection using a variety of intuitive techniques like tap, gesture, * manipulate a selection of items in a RecyclerView instance using a variety of
* and mouse lasso. * intuitive techniques like tap, gesture, and mouse-based band selection (aka 'lasso').
* *
* <p> * <p>
* Example usage: * Building a bare-bones instance:
* <pre>SelectionTracker<Uri> tracker = new SelectionTracker.Builder<>( *
* "my-uri-selection", * <pre>{@code
* recyclerView, * SelectionTracker<Uri> tracker = new SelectionTracker.Builder<>(
* new DemoStableIdProvider(recyclerView.getAdapter()), * "my-uri-selection",
* new MyDetailsLookup(recyclerView), * recyclerView,
* StorageStrategy.createParcelableStorage(Uri.class)) * new YourItemKeyProvider(recyclerView.getAdapter()),
* .build(); * new YourItemDetailsLookup(recyclerView),
* </pre> * StorageStrategy.createParcelableStorage(Uri.class))
* .build();
* }</pre>
* *
* <p> * <p>
* <b>Restricting which items can be selected and limiting selection size</b> * <b>Restricting which items can be selected and limiting selection size</b>
* *
* <p> * <p>
* {@link SelectionPredicate} provides a mechanism to restrict which Items can be selected, * {@link SelectionPredicate} and
* to limit the number of items that can be selected, as well as allowing the selection * {@link SelectionTracker.Builder#withSelectionPredicate(SelectionPredicate)}
* code to be placed into "single select" mode, which as the name indicates, constrains * together provide a mechanism for restricting which items can be selected and
* the selection size to a single item. * limiting selection size. Use {@link SelectionPredicates#createSelectSingleAnything()}
* * for single-selection, or write your own {@link SelectionPredicate} if other
* <p>Configuring the tracker for single single selection support can be done * constraints are required.
* by supplying {@link SelectionPredicates#createSelectSingleAnything()}. *
* * <pre>{@code
* SelectionTracker<String> tracker = new SelectionTracker.Builder<>( * SelectionTracker<String> tracker = new SelectionTracker.Builder<>(
* "my-string-selection", * "my-string-selection",
* recyclerView, * recyclerView,
* new DemoStableIdProvider(recyclerView.getAdapter()), * new YourItemKeyProvider(recyclerView.getAdapter()),
* new MyDetailsLookup(recyclerView), * new YourItemDetailsLookup(recyclerView),
* StorageStrategy.createStringStorage()) * StorageStrategy.createStringStorage())
* .withSelectionPredicate(SelectionPredicates#createSelectSingleAnything()) * .withSelectionPredicate(SelectionPredicates#createSelectSingleAnything())
* .build(); * .build();
* </pre> * }</pre>
*
* <p> * <p>
* <b>Retaining state across Android lifecycle events</b> * <b>Retaining state across Android lifecycle events</b>
* *
* <p> * <p>
* Support for storage/persistence of selection must be configured and invoked manually * Support for storage/persistence of selection must be configured and invoked manually
* owing to its reliance on Activity lifecycle events. * owing to its reliance on Activity lifecycle events.
* Failure to include support for selection storage will result in the active selection * Failure to include support for selection storage will result in selection
* being lost when the Activity receives a configuration change (e.g. rotation) * being lost when the Activity receives a configuration change (e.g. rotation),
* or when the application process is destroyed by the OS to reclaim resources. * or when the application is paused or stopped. For this reason
* {@link StorageStrategy} is a required argument to obtain a {@link Builder}
* instance.
* *
* <p> * <p>
* <b>Key Type</b> * <b>Key Type</b>
* *
* <p> * <p>
* Developers must decide on the key type used to identify selected items. Support * A developer must decide on the key type used to identify selected items.
* is provided for three types: {@link Parcelable}, {@link String}, and {@link Long}. * Support is provided for three types: {@link Parcelable}, {@link String}, and {@link Long}.
* *
* <p> * <p>
* {@link Parcelable}: Any Parcelable type can be used as the selection key. This is especially * {@link Parcelable}: Any Parcelable type can be used as the selection key. This is especially
@ -450,30 +451,55 @@ public abstract class SelectionTracker<K> {
* {@link String}: Use String when a string based stable identifier is available. * {@link String}: Use String when a string based stable identifier is available.
* *
* <p> * <p>
* {@link Long}: Use Long when RecyclerView's long stable ids are * {@link Long}: Use Long when a project is already employing RecyclerView's built-in
* already in use. It comes with some limitations, however, as access to stable ids * support for stable ids. In this case you may choose to use {@link StableIdKeyProvider}
* at runtime is limited. Band selection support is not available when using the default * to supply selection keys to the SelectionTracker based on data already accessible
* long key storage implementation. See {@link StableIdKeyProvider} for details. * in RecyclerView and it's Adapter.
*
* See {@link StableIdKeyProvider} for important details and limitations (<i>and a suggestion
* that you might just want to write your own {@link ItemKeyProvider}. It's easy!</i>)
* See the "Gotchas" selection below for details on selection size limits.
* *
* <p> * <p>
* Usage: * Usage:
* *
* <pre> * <pre>{@code
* private SelectionTracker<Uri> mTracker; * private SelectionTracker<Uri> tracker;
* *
* public void onCreate(Bundle savedInstanceState) { * public void onCreate(Bundle savedInstanceState) {
* // See above for details on constructing a SelectionTracker instance. * if (savedInstanceState != null) {
* * tracker.onRestoreInstanceState(savedInstanceState);
* if (savedInstanceState != null) { * }
* mTracker.onRestoreInstanceState(savedInstanceState);
* }
* } * }
* *
* protected void onSaveInstanceState(Bundle outState) { * protected void onSaveInstanceState(Bundle outState) {
* super.onSaveInstanceState(outState); * super.onSaveInstanceState(outState);
* mTracker.onSaveInstanceState(outState); * tracker.onSaveInstanceState(outState);
* } * }
* </pre> * }</pre>
*
* <p>
* <b>Gotchas</b>
*
* <p>TransactionTooLargeException:
*
* <p>Many factors affect the maximum number of items that can be persisted when the
* application is paused or stopped. Unfortunately that number is not deterministic as it
* depends on the size of the key type used for selection, the number of selected items, and
* external demand on system resources. For that reason it is best to use the smallest viable
* key type, and to enforce a limit on the number of items that can be selected.
*
* <p>Furthermore the inability to persist a selection during a lifecycle event will result
* in a android.os.{@link android.os.TransactionTooLargeException}. See
* http://issuetracker.google.com/168706011 for details.
*
* <p>ItemTouchHelper
*
* <p>When using {@link SelectionTracker} along side an
* {@link androidx.recyclerview.widget.ItemTouchHelper} with the same RecyclerView instance
* the SelectionTracker instance must be created and installed before the ItemTouchHelper.
* Failure to do so will result in unintended selections during item drag operations, and
* possibly other situations.
* *
* @param <K> Selection key type. Built in support is provided for {@link String}, * @param <K> Selection key type. Built in support is provided for {@link String},
* {@link Long}, and {@link Parcelable}. {@link StorageStrategy} * {@link Long}, and {@link Parcelable}. {@link StorageStrategy}
@ -496,7 +522,7 @@ public abstract class SelectionTracker<K> {
private ItemKeyProvider<K> mKeyProvider; private ItemKeyProvider<K> mKeyProvider;
private ItemDetailsLookup<K> mDetailsLookup; private ItemDetailsLookup<K> mDetailsLookup;
private FocusDelegate<K> mFocusDelegate = FocusDelegate.dummy(); private FocusDelegate<K> mFocusDelegate = FocusDelegate.stub();
private OnItemActivatedListener<K> mOnItemActivatedListener; private OnItemActivatedListener<K> mOnItemActivatedListener;
private OnDragInitiatedListener mOnDragInitiatedListener; private OnDragInitiatedListener mOnDragInitiatedListener;
@ -646,12 +672,11 @@ public abstract class SelectionTracker<K> {
* *
* @param toolTypes the tool types to be used * @param toolTypes the tool types to be used
* @return this * @return this
*
* @deprecated GestureSelection is best bound to {@link MotionEvent#TOOL_TYPE_FINGER}, * @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. * and only that tool type. This method will be removed in a future release.
*/ */
//@Deprecated //@Deprecated
public @NonNull Builder<K> withGestureTooltypes(@NonNull int... toolTypes) { public @NonNull Builder<K> withGestureTooltypes(int @NonNull ... toolTypes) {
Log.w(TAG, "Setting gestureTooltypes is likely to result in unexpected behavior."); Log.w(TAG, "Setting gestureTooltypes is likely to result in unexpected behavior.");
mGestureToolTypes = toolTypes; mGestureToolTypes = toolTypes;
return this; return this;
@ -685,12 +710,11 @@ public abstract class SelectionTracker<K> {
* *
* @param toolTypes the tool types to be used * @param toolTypes the tool types to be used
* @return this * @return this
*
* @deprecated PointerSelection is best bound to {@link MotionEvent#TOOL_TYPE_MOUSE}, * @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. * and only that tool type. This method will be removed in a future release.
*/ */
@Deprecated //@Deprecated
public @NonNull Builder<K> withPointerTooltypes(@NonNull int... toolTypes) { public @NonNull Builder<K> withPointerTooltypes(int @NonNull ... toolTypes) {
Log.w(TAG, "Setting pointerTooltypes is likely to result in unexpected behavior."); Log.w(TAG, "Setting pointerTooltypes is likely to result in unexpected behavior.");
mPointerToolTypes = toolTypes; mPointerToolTypes = toolTypes;
return this; return this;
@ -709,7 +733,7 @@ public abstract class SelectionTracker<K> {
// Event glue between RecyclerView and SelectionTracker keeps the classes separate // Event glue between RecyclerView and SelectionTracker keeps the classes separate
// so that a SelectionTracker can be shared across RecyclerView instances that // so that a SelectionTracker can be shared across RecyclerView instances that
// represent the same data in different ways. // represent the same data in different ways.
EventBridge.install(mAdapter, tracker, mKeyProvider); EventBridge.install(mAdapter, tracker, mKeyProvider, mRecyclerView::post);
// Scroller is stateful and can be reset, but we don't manage it directly. // Scroller is stateful and can be reset, but we don't manage it directly.
// GestureSelectionHelper will reset scroller when it is reset. // GestureSelectionHelper will reset scroller when it is reset.
@ -728,19 +752,29 @@ public abstract class SelectionTracker<K> {
GestureDetector gestureDetector = new GestureDetector(mContext, gestureRouter); GestureDetector gestureDetector = new GestureDetector(mContext, gestureRouter);
// GestureSelectionHelper provides logic that interprets a combination // GestureSelectionHelper provides logic that interprets a combination
// of motions and gestures in order to provide gesture driven selection support // of motions and gestures in order to provide fluid "long-press and drag"
// when used in conjunction with RecyclerView. // finger driven selection support.
final GestureSelectionHelper gestureHelper = GestureSelectionHelper.create( final GestureSelectionHelper gestureSelectionHelper = GestureSelectionHelper.create(
tracker, mSelectionPredicate, mRecyclerView, scroller, mMonitor); tracker, mSelectionPredicate, mRecyclerView, scroller, mMonitor);
// EventRouter receives events for RecyclerView, dispatching to handlers // EventRouter receives events for RecyclerView, dispatching to handlers
// registered by tool-type. // registered by tool-type.
EventRouter eventRouter = new EventRouter(); EventRouter eventRouter = new EventRouter();
GestureDetectorWrapper gestureDetectorWrapper =
new GestureDetectorWrapper(gestureDetector);
// Temp fix for b/166836317.
// TODO: Add support for multiple listeners per tool type to EventRouter, then
// register backstop with primary router.
EventRouter backstopRouter = new EventRouter();
EventBackstop backstop = new EventBackstop();
DisallowInterceptFilter backstopWrapper = new DisallowInterceptFilter(backstop);
backstopRouter.set(new ToolSourceKey(MotionEvent.TOOL_TYPE_UNKNOWN), backstopWrapper);
// Finally hook the framework up to listening to RecycleView events. // Finally hook the framework up to listening to RecycleView events.
mRecyclerView.addOnItemTouchListener(eventRouter); mRecyclerView.addOnItemTouchListener(eventRouter);
mRecyclerView.addOnItemTouchListener( mRecyclerView.addOnItemTouchListener(gestureDetectorWrapper);
new GestureDetectorOnItemTouchListenerAdapter(gestureDetector)); mRecyclerView.addOnItemTouchListener(backstopRouter);
// Reset manager listens for cancel events from RecyclerView. In response to that it // Reset manager listens for cancel events from RecyclerView. In response to that it
// advises other classes it is time to reset state. // advises other classes it is time to reset state.
@ -750,20 +784,27 @@ public abstract class SelectionTracker<K> {
// //
// 1. Monitor selection reset which can be invoked by clients in response // 1. Monitor selection reset which can be invoked by clients in response
// to back key press and some application lifecycle events. // 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()); tracker.addObserver(resetMgr.getSelectionObserver());
// ...and 2. Monitor ACTION_CANCEL events (which arrive exclusively
// via TOOL_TYPE_UNKNOWN).
//
// CAUTION! Registering resetMgr directly with RecyclerView#addOnItemTouchListener // CAUTION! Registering resetMgr directly with RecyclerView#addOnItemTouchListener
// will not work as expected. Once EventRouter returns true, RecyclerView will // will not work as expected. Once EventRouter returns true, RecyclerView will
// no longer dispatch any events to other listeners for the duration of the // no longer dispatch any events to other listeners for the duration of the
// stream, not even ACTION_CANCEL events. // stream, not even ACTION_CANCEL events.
eventRouter.set(MotionEvent.TOOL_TYPE_UNKNOWN, resetMgr.getInputListener()); eventRouter.set(new ToolSourceKey(MotionEvent.TOOL_TYPE_UNKNOWN),
resetMgr.getInputListener());
// Finally register all of the Resettables.
resetMgr.addResetHandler(tracker); resetMgr.addResetHandler(tracker);
resetMgr.addResetHandler(mMonitor.asResettable()); resetMgr.addResetHandler(mMonitor.asResettable());
resetMgr.addResetHandler(gestureHelper); resetMgr.addResetHandler(gestureSelectionHelper);
resetMgr.addResetHandler(gestureDetectorWrapper);
resetMgr.addResetHandler(eventRouter);
resetMgr.addResetHandler(backstopRouter);
resetMgr.addResetHandler(backstop);
resetMgr.addResetHandler(backstopWrapper);
// But before you move on, there's more work to do. Event plumbing has been // 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. // installed, but we haven't registered any of our helpers or callbacks.
@ -774,7 +815,7 @@ public abstract class SelectionTracker<K> {
// be configured to handle other types of input (to satisfy user expectation).); // be configured to handle other types of input (to satisfy user expectation).);
// Internally, the code doesn't permit nullable listeners, so we lazily // Internally, the code doesn't permit nullable listeners, so we lazily
// initialize dummy instances if the developer didn't supply a real listener. // initialize stub instances if the developer didn't supply a real listener.
mOnDragInitiatedListener = (mOnDragInitiatedListener != null) mOnDragInitiatedListener = (mOnDragInitiatedListener != null)
? mOnDragInitiatedListener ? mOnDragInitiatedListener
: new OnDragInitiatedListener() { : new OnDragInitiatedListener() {
@ -789,7 +830,7 @@ public abstract class SelectionTracker<K> {
: new OnItemActivatedListener<K>() { : new OnItemActivatedListener<K>() {
@Override @Override
public boolean onItemActivated( public boolean onItemActivated(
@NonNull ItemDetailsLookup.ItemDetails<K> item, ItemDetailsLookup.@NonNull ItemDetails<K> item,
@NonNull MotionEvent e) { @NonNull MotionEvent e) {
return false; return false;
} }
@ -811,18 +852,7 @@ public abstract class SelectionTracker<K> {
mKeyProvider, mKeyProvider,
mDetailsLookup, mDetailsLookup,
mSelectionPredicate, mSelectionPredicate,
new Runnable() { gestureSelectionHelper::start,
@Override
public void run() {
if (mSelectionPredicate.canSelectMultiple()) {
try {
gestureHelper.start();
} catch (Throwable ex) {
eu.faircode.email.Log.e(ex);
}
}
}
},
mOnDragInitiatedListener, mOnDragInitiatedListener,
mOnItemActivatedListener, mOnItemActivatedListener,
mFocusDelegate, mFocusDelegate,
@ -831,11 +861,14 @@ public abstract class SelectionTracker<K> {
public void run() { public void run() {
mRecyclerView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); mRecyclerView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
} }
}); },
// Provide temporary glue to address b/166836317
backstop::onLongPress);
for (int toolType : mGestureToolTypes) { for (int toolType : mGestureToolTypes) {
gestureRouter.register(toolType, touchHandler); ToolSourceKey key = new ToolSourceKey(toolType);
eventRouter.set(toolType, gestureHelper); gestureRouter.register(key, touchHandler);
eventRouter.set(key, gestureSelectionHelper);
} }
// Provides high level glue for binding mouse events and gestures // Provides high level glue for binding mouse events and gestures
@ -849,10 +882,14 @@ public abstract class SelectionTracker<K> {
mFocusDelegate); mFocusDelegate);
for (int toolType : mPointerToolTypes) { for (int toolType : mPointerToolTypes) {
gestureRouter.register(toolType, mouseHandler); gestureRouter.register(new ToolSourceKey(toolType), mouseHandler);
} }
@Nullable BandSelectionHelper<K> bandHelper = null; ToolSourceKey touchpadKey = new ToolSourceKey(MotionEvent.TOOL_TYPE_FINGER,
InputDevice.SOURCE_MOUSE);
gestureRouter.register(touchpadKey, mouseHandler);
BandSelectionHelper<K> bandHelper = null;
// Band selection not supported in single select mode, or when key access // Band selection not supported in single select mode, or when key access
// is limited to anything less than the entire corpus. // is limited to anything less than the entire corpus.
@ -880,7 +917,8 @@ public abstract class SelectionTracker<K> {
OnItemTouchListener pointerEventHandler = new PointerDragEventInterceptor( OnItemTouchListener pointerEventHandler = new PointerDragEventInterceptor(
mDetailsLookup, mOnDragInitiatedListener, bandHelper); mDetailsLookup, mOnDragInitiatedListener, bandHelper);
eventRouter.set(MotionEvent.TOOL_TYPE_MOUSE, pointerEventHandler); eventRouter.set(new ToolSourceKey(MotionEvent.TOOL_TYPE_MOUSE), pointerEventHandler);
eventRouter.set(touchpadKey, pointerEventHandler);
return tracker; return tracker;
} }

@ -16,27 +16,44 @@
package androidx.recyclerview.selection; package androidx.recyclerview.selection;
import static androidx.core.util.Preconditions.checkArgument;
import static androidx.core.util.Preconditions.checkNotNull;
import static androidx.recyclerview.selection.Shared.DEBUG; import static androidx.recyclerview.selection.Shared.DEBUG;
import android.util.Log; import android.util.Log;
import android.util.SparseArray; import android.util.SparseArray;
import android.view.View; import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.collection.LongSparseArray; import androidx.collection.LongSparseArray;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.OnChildAttachStateChangeListener; import androidx.recyclerview.widget.RecyclerView.OnChildAttachStateChangeListener;
import androidx.recyclerview.widget.RecyclerView.RecyclerListener;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
/** /**
* An {@link ItemKeyProvider} that provides stable ids by way of cached * An {@link ItemKeyProvider} that provides item keys by way of native
* {@link RecyclerView.Adapter} stable ids. Items enter the cache as they are laid out by * {@link RecyclerView.Adapter} stable ids.
* RecyclerView, and are removed from the cache as they are recycled. *
* <p>The corresponding RecyclerView.Adapter instance must:
* <ol>
* <li> Enable stable ids using {@link RecyclerView.Adapter#setHasStableIds(boolean)}
* <li> Override {@link RecyclerView.Adapter#getItemId(int)} with a real implementation.
* </ol>
* *
* <p> * <p>
* There are trade-offs with this implementation as it necessarily auto-boxes {@code long} * There are trade-offs with this implementation:
* stable id values into {@code Long} values for use as selection keys. The core Selection API * <ul>
* uses a parameterized key type to permit other keys (such as Strings or URIs). * <li>It necessarily auto-boxes {@code long} stable id values into {@code Long} values for
* use as selection keys.
* <li>It deprives Chromebook users (actually, any device with an attached pointer) of support
* for band-selection.
* </ul>
*
* <p>See com.example.android.supportv7.widget.selection.fancy.DemoAdapter.KeyProvider in the
* SupportV7 Demos package for an example of how to implement a better ItemKeyProvider.
*/ */
public final class StableIdKeyProvider extends ItemKeyProvider<Long> { public final class StableIdKeyProvider extends ItemKeyProvider<Long> {
@ -44,48 +61,57 @@ public final class StableIdKeyProvider extends ItemKeyProvider<Long> {
private final SparseArray<Long> mPositionToKey = new SparseArray<>(); private final SparseArray<Long> mPositionToKey = new SparseArray<>();
private final LongSparseArray<Integer> mKeyToPosition = new LongSparseArray<>(); private final LongSparseArray<Integer> mKeyToPosition = new LongSparseArray<>();
private final RecyclerView mRecyclerView; private final ViewHost mHost;
/**
* Creates a new key provider that uses cached {@code long} stable ids associated
* with the RecyclerView items.
*
* @param recyclerView the owner RecyclerView
*/
public StableIdKeyProvider(@NonNull RecyclerView recyclerView) {
// Since this provide is based on stable ids based on whats laid out in the window StableIdKeyProvider(@NonNull ViewHost host) {
// we can only satisfy "window" scope key access. // Provider is based on the stable ids provided by ViewHolders which
// are only accessible when the holders are attached or yet-to-be-recycled.
// For that reason we can only satisfy "CACHED" scope key access which
// limits library features such as mouse-driven band selection.
super(SCOPE_CACHED); super(SCOPE_CACHED);
mRecyclerView = recyclerView; checkNotNull(host);
mHost = host;
mRecyclerView.addOnChildAttachStateChangeListener( mHost.registerLifecycleListener(
new OnChildAttachStateChangeListener() { new ViewHost.LifecycleListener() {
@Override @Override
public void onChildViewAttachedToWindow(View view) { public void onAttached(@NonNull View view) {
onAttached(view); StableIdKeyProvider.this.onAttached(view);
} }
@Override @Override
public void onChildViewDetachedFromWindow(View view) { public void onRecycled(@NonNull View view) {
onDetached(view); StableIdKeyProvider.this.onRecycled(view);
} }
} }
); );
}
/**
* Creates a new key provider that uses cached {@code long} stable ids associated
* with the RecyclerView items.
*
* @param recyclerView the owner RecyclerView
*/
public StableIdKeyProvider(@NonNull RecyclerView recyclerView) {
this(new DefaultViewHost(recyclerView));
// Adapters used w/ StableIdKeyProvider MUST have StableIds enabled.
checkArgument(recyclerView.getAdapter().hasStableIds(), "RecyclerView"
+ ".Adapter#hasStableIds must return true.");
} }
@SuppressWarnings("WeakerAccess") /* synthetic access */ @SuppressWarnings("WeakerAccess") /* synthetic access */
void onAttached(@NonNull View view) { void onAttached(@NonNull View view) {
RecyclerView.ViewHolder holder = mRecyclerView.findContainingViewHolder(view); ViewHolder holder = mHost.findViewHolder(view);
if (holder == null) { if (holder == null) {
if (DEBUG) { if (DEBUG) {
Log.w(TAG, "Unable to find ViewHolder for View. Ignoring onAttached event."); Log.w(TAG, "Unable to find ViewHolder for View. Ignoring onAttached event.");
} }
return; return;
} }
int position = holder.getAbsoluteAdapterPosition(); int position = mHost.getPosition(holder);
long id = holder.getItemId(); long id = holder.getItemId();
if (position != RecyclerView.NO_POSITION && id != RecyclerView.NO_ID) { if (position != RecyclerView.NO_POSITION && id != RecyclerView.NO_ID) {
mPositionToKey.put(position, id); mPositionToKey.put(position, id);
@ -94,15 +120,15 @@ public final class StableIdKeyProvider extends ItemKeyProvider<Long> {
} }
@SuppressWarnings("WeakerAccess") /* synthetic access */ @SuppressWarnings("WeakerAccess") /* synthetic access */
void onDetached(@NonNull View view) { void onRecycled(@NonNull View view) {
RecyclerView.ViewHolder holder = mRecyclerView.findContainingViewHolder(view); ViewHolder holder = mHost.findViewHolder(view);
if (holder == null) { if (holder == null) {
if (DEBUG) { if (DEBUG) {
Log.w(TAG, "Unable to find ViewHolder for View. Ignoring onDetached event."); Log.w(TAG, "Unable to find ViewHolder for View. Ignoring onDetached event.");
} }
return; return;
} }
int position = holder.getAbsoluteAdapterPosition(); int position = mHost.getPosition(holder);
long id = holder.getItemId(); long id = holder.getItemId();
if (position != RecyclerView.NO_POSITION && id != RecyclerView.NO_ID) { if (position != RecyclerView.NO_POSITION && id != RecyclerView.NO_ID) {
mPositionToKey.delete(position); mPositionToKey.delete(position);
@ -112,6 +138,8 @@ public final class StableIdKeyProvider extends ItemKeyProvider<Long> {
@Override @Override
public @Nullable Long getKey(int position) { public @Nullable Long getKey(int position) {
// TODO: Consider using RecyclerView.NO_ID for consistency w/ getPosition impl.
// Currently GridModel impl depends on null return values.
return mPositionToKey.get(position, null); return mPositionToKey.get(position, null);
} }
@ -119,4 +147,91 @@ public final class StableIdKeyProvider extends ItemKeyProvider<Long> {
public int getPosition(@NonNull Long key) { public int getPosition(@NonNull Long key) {
return mKeyToPosition.get(key, RecyclerView.NO_POSITION); return mKeyToPosition.get(key, RecyclerView.NO_POSITION);
} }
/**
* A wrapper interface for RecyclerView allowing for easy unit testing.
*/
interface ViewHost {
/** Registers View{Holder} lifecycle event listener. */
void registerLifecycleListener(@NonNull LifecycleListener listener);
/**
* Returns the ViewHolder containing {@code View}.
*/
@Nullable ViewHolder findViewHolder(@NonNull View view);
/**
* Returns the position of the ViewHolder, or RecyclerView.NO_POSITION
* if unknown.
*
* This method supports testing of StableIdKeyProvider independent of
* a real RecyclerView instance. The correct runtime implementation is
* {@code return holder.getAbsoluteAdapterPosition}. This implementation
* depends on a concrete RecyclerView instance, which isn't test friendly
* given the testing approach in StableIdKeyProviderTest. Thus the
* introduction of this interface method allowing a test double to
* supply adapter position as needed to test.
*/
int getPosition(@NonNull ViewHolder holder);
/** A View{Holder} lifecycle listener interface. */
interface LifecycleListener {
/** Called when view is attached. */
void onAttached(@NonNull View view);
/** Called when view is recycled. */
void onRecycled(@NonNull View view);
}
}
/**
* Implementation of ViewHost that wraps a RecyclerView instance.
*/
private static class DefaultViewHost implements ViewHost {
private final @NonNull RecyclerView mRecyclerView;
DefaultViewHost(@NonNull RecyclerView recyclerView) {
checkNotNull(recyclerView);
mRecyclerView = recyclerView;
}
@Override
public void registerLifecycleListener(@NonNull LifecycleListener listener) {
mRecyclerView.addOnChildAttachStateChangeListener(
new OnChildAttachStateChangeListener() {
@Override
public void onChildViewAttachedToWindow(@NonNull View view) {
listener.onAttached(view);
}
@Override
public void onChildViewDetachedFromWindow(@NonNull View view) {
// Cached position <> key data is discarded only when
// a view is recycled. See b/145767095 for details.
}
}
);
mRecyclerView.addRecyclerListener(
new RecyclerListener() {
@Override
public void onViewRecycled(@NonNull ViewHolder holder) {
listener.onRecycled(holder.itemView);
}
}
);
}
@Override
public @Nullable ViewHolder findViewHolder(@NonNull View view) {
return mRecyclerView.findContainingViewHolder(view);
}
@Override
public int getPosition(@NonNull ViewHolder holder) {
return holder.getAbsoluteAdapterPosition();
}
}
} }

@ -21,10 +21,11 @@ import static androidx.core.util.Preconditions.checkArgument;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable; import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
/** /**
@ -87,7 +88,7 @@ public abstract class StorageStrategy<K> {
* @return StorageStrategy suitable for use with {@link Parcelable} keys * @return StorageStrategy suitable for use with {@link Parcelable} keys
* (like {@link android.net.Uri}). * (like {@link android.net.Uri}).
*/ */
public static @NonNull <K extends Parcelable> StorageStrategy<K> createParcelableStorage( public static <K extends Parcelable> @NonNull StorageStrategy<K> createParcelableStorage(
@NonNull Class<K> type) { @NonNull Class<K> type) {
return new ParcelableStorageStrategy<>(type); return new ParcelableStorageStrategy<>(type);
} }
@ -120,7 +121,7 @@ public abstract class StorageStrategy<K> {
return null; return null;
} }
@Nullable ArrayList<String> stored = state.getStringArrayList(SELECTION_ENTRIES); ArrayList<String> stored = state.getStringArrayList(SELECTION_ENTRIES);
if (stored == null) { if (stored == null) {
return null; return null;
} }
@ -158,7 +159,7 @@ public abstract class StorageStrategy<K> {
return null; return null;
} }
@Nullable long[] stored = state.getLongArray(SELECTION_ENTRIES); long[] stored = state.getLongArray(SELECTION_ENTRIES);
if (stored == null) { if (stored == null) {
return null; return null;
} }
@ -196,6 +197,7 @@ public abstract class StorageStrategy<K> {
} }
@Override @Override
@SuppressWarnings("deprecation")
public @Nullable Selection<K> asSelection(@NonNull Bundle state) { public @Nullable Selection<K> asSelection(@NonNull Bundle state) {
String keyType = state.getString(SELECTION_KEY_TYPE, null); String keyType = state.getString(SELECTION_KEY_TYPE, null);
@ -203,7 +205,7 @@ public abstract class StorageStrategy<K> {
return null; return null;
} }
@Nullable ArrayList<K> stored = state.getParcelableArrayList(SELECTION_ENTRIES); ArrayList<K> stored = state.getParcelableArrayList(SELECTION_ENTRIES);
if (stored == null) { if (stored == null) {
return null; return null;
} }

@ -18,14 +18,15 @@ package androidx.recyclerview.selection;
import android.view.MotionEvent; import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import org.jspecify.annotations.NonNull;
/** /**
* No-op implementation of OnItemTouchListener suitable for use as a default * No-op implementation of OnItemTouchListener suitable for use as a default
* handler w/ ToolHandlerRegistery, or in tests. * handler w/ ToolHandlerRegistery, or in tests.
*/ */
final class DummyOnItemTouchListener implements RecyclerView.OnItemTouchListener { final class StubOnItemTouchListener implements RecyclerView.OnItemTouchListener {
@Override @Override
public boolean onInterceptTouchEvent( public boolean onInterceptTouchEvent(
@NonNull RecyclerView unused, @NonNull MotionEvent e) { @NonNull RecyclerView unused, @NonNull MotionEvent e) {

@ -1,72 +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 static androidx.core.util.Preconditions.checkState;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Arrays;
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 were a Map used.k
*
* <p>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 <T> type of item being registered.
*/
final class ToolHandlerRegistry<T> {
// 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<T> mHandlers = Arrays.asList(null, null, null, null, null);
private final T mDefault;
ToolHandlerRegistry(@NonNull T defaultDelegate) {
checkArgument(defaultDelegate != null);
mDefault = defaultDelegate;
}
/**
* @param toolType
* @param delegate the delegate, or null to unregister.
* @throws IllegalStateException if an tooltype handler is already registered.
*/
void set(int toolType, @Nullable T delegate) {
checkArgument(toolType >= 0 && toolType <= MotionEvent.TOOL_TYPE_ERASER);
checkState(mHandlers.get(toolType) == null);
mHandlers.set(toolType, delegate);
}
T get(@NonNull MotionEvent e) {
T d = mHandlers.get(e.getToolType(0));
return d != null ? d : mDefault;
}
}

@ -0,0 +1,82 @@
/*
* 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 org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
/**
* Registry for event handlers. This is keyed by a ToolSourceKey which allows for searching for a
* handler which handles both a specific SOURCE + TOOL and if none exists, will fall back to
* TOOL only handlers. If none exists, a default handler will be returned instead.
*
* <p>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 ToolSourceKey has not be registered using
* {@link ToolSourceHandlerRegistry#set(ToolSourceKey, T)}.
*
* @param <T> type of item being registered.
*/
final class ToolSourceHandlerRegistry<T> {
/**
* A map that is keyed by a ToolSourceKey which contains either a TOOL + SOURCE or just a
* TOOL. This allows for handlers to get routed to more specific handlers before falling back
* to less specific and finally the default one.
*/
private final Map<ToolSourceKey, T> mHandlers = new HashMap<ToolSourceKey, T>();
private final T mDefault;
ToolSourceHandlerRegistry(@NonNull T defaultDelegate) {
checkArgument(defaultDelegate != null);
mDefault = defaultDelegate;
}
/**
* @param delegate the delegate, or null to unregister.
* @throws IllegalStateException if a key already has a registered handler.
*/
void set(@NonNull ToolSourceKey key, @Nullable T delegate) {
if (delegate == null && mHandlers.containsKey(key)) {
mHandlers.remove(key);
return;
}
mHandlers.put(key, delegate);
}
T get(@NonNull MotionEvent e) {
ToolSourceKey key = ToolSourceKey.fromMotionEvent(e);
T d = mHandlers.get(key);
if (d == null) {
// If the map of handlers doesn't contain a specific MotionEventKey(tool, source)
// then fallback to the less specific MotionEventKey(tool).
d = mHandlers.get(new ToolSourceKey(key.getToolType()));
}
return d != null ? d : mDefault;
}
}

@ -0,0 +1,114 @@
/*
* Copyright 2024 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 android.view.InputDevice.SOURCE_MOUSE;
import static android.view.InputDevice.SOURCE_UNKNOWN;
import static android.view.MotionEvent.TOOL_TYPE_ERASER;
import static android.view.MotionEvent.TOOL_TYPE_FINGER;
import static android.view.MotionEvent.TOOL_TYPE_MOUSE;
import static android.view.MotionEvent.TOOL_TYPE_STYLUS;
import static android.view.MotionEvent.TOOL_TYPE_UNKNOWN;
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import android.view.InputDevice;
import android.view.MotionEvent;
import androidx.annotation.IntDef;
import androidx.annotation.RestrictTo;
import org.jspecify.annotations.NonNull;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Objects;
/**
* Enables storing multiple {@link MotionEvent} parameters (e.g.
* {@link MotionEvent#getToolType(int)}) as a key in a map. This opens up the ability to map
* these multiple parameters against their respective handlers. For example some events behave
* differently based on their toolType and source where others just require toolType.
*/
@RestrictTo(LIBRARY)
public class ToolSourceKey {
private final @ToolType int mToolType;
private final @Source int mSource;
ToolSourceKey(@ToolType int toolType) {
mToolType = toolType;
mSource = InputDevice.SOURCE_UNKNOWN;
}
ToolSourceKey(@ToolType int toolType, @Source int source) {
mToolType = toolType;
mSource = source;
}
/**
* Create a `ToolSourceKey` from a supplied `MotionEvent`.
*
* @return {@link ToolSourceKey}
*/
public static @NonNull ToolSourceKey fromMotionEvent(@NonNull MotionEvent e) {
return new ToolSourceKey(e.getToolType(0), e.getSource());
}
public int getToolType() {
return mToolType;
}
public int getSource() {
return mSource;
}
@Override
public int hashCode() {
return Objects.hash(mToolType, mSource);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof ToolSourceKey)) {
return false;
}
ToolSourceKey matcher = (ToolSourceKey) obj;
return mToolType == matcher.getToolType() && mSource == matcher.getSource();
}
@Override
public @NonNull String toString() {
return String.valueOf(mToolType) + "," + String.valueOf(mSource);
}
@IntDef(value = {TOOL_TYPE_FINGER, TOOL_TYPE_MOUSE, TOOL_TYPE_ERASER, TOOL_TYPE_STYLUS,
TOOL_TYPE_UNKNOWN})
@Retention(RetentionPolicy.SOURCE)
@interface ToolType {
}
/**
* Please add additional sources here from InputDevice.SOURCE_*.
*/
@IntDef(value = {SOURCE_MOUSE, SOURCE_UNKNOWN})
@Retention(RetentionPolicy.SOURCE)
@interface Source {
}
}

@ -22,11 +22,12 @@ import static androidx.recyclerview.selection.Shared.DEBUG;
import android.util.Log; import android.util.Log;
import android.view.MotionEvent; import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails; import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate; import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import org.jspecify.annotations.NonNull;
/** /**
* A MotionInputHandler that provides the high-level glue for touch driven selection. This class * A MotionInputHandler that provides the high-level glue for touch driven selection. This class
* works with {@link RecyclerView}, {@link GestureRouter}, and {@link GestureSelectionHelper} to * works with {@link RecyclerView}, {@link GestureRouter}, and {@link GestureSelectionHelper} to
@ -36,7 +37,7 @@ import androidx.recyclerview.widget.RecyclerView;
*/ */
final class TouchInputHandler<K> extends MotionInputHandler<K> { final class TouchInputHandler<K> extends MotionInputHandler<K> {
private static final String TAG = "TouchInputDelegate"; private static final String TAG = "TouchInputHandler";
private final ItemDetailsLookup<K> mDetailsLookup; private final ItemDetailsLookup<K> mDetailsLookup;
private final SelectionPredicate<K> mSelectionPredicate; private final SelectionPredicate<K> mSelectionPredicate;
@ -44,6 +45,7 @@ final class TouchInputHandler<K> extends MotionInputHandler<K> {
private final OnDragInitiatedListener mOnDragInitiatedListener; private final OnDragInitiatedListener mOnDragInitiatedListener;
private final Runnable mGestureStarter; private final Runnable mGestureStarter;
private final Runnable mHapticPerformer; private final Runnable mHapticPerformer;
private final Runnable mLongPressCallback;
TouchInputHandler( TouchInputHandler(
@NonNull SelectionTracker<K> selectionTracker, @NonNull SelectionTracker<K> selectionTracker,
@ -54,7 +56,8 @@ final class TouchInputHandler<K> extends MotionInputHandler<K> {
@NonNull OnDragInitiatedListener onDragInitiatedListener, @NonNull OnDragInitiatedListener onDragInitiatedListener,
@NonNull OnItemActivatedListener<K> onItemActivatedListener, @NonNull OnItemActivatedListener<K> onItemActivatedListener,
@NonNull FocusDelegate<K> focusDelegate, @NonNull FocusDelegate<K> focusDelegate,
@NonNull Runnable hapticPerformer) { @NonNull Runnable hapticPerformer,
@NonNull Runnable longPressCallback) {
super(selectionTracker, keyProvider, focusDelegate); super(selectionTracker, keyProvider, focusDelegate);
@ -71,6 +74,7 @@ final class TouchInputHandler<K> extends MotionInputHandler<K> {
mOnItemActivatedListener = onItemActivatedListener; mOnItemActivatedListener = onItemActivatedListener;
mOnDragInitiatedListener = onDragInitiatedListener; mOnDragInitiatedListener = onDragInitiatedListener;
mHapticPerformer = hapticPerformer; mHapticPerformer = hapticPerformer;
mLongPressCallback = longPressCallback;
} }
@Override @Override
@ -80,16 +84,10 @@ final class TouchInputHandler<K> extends MotionInputHandler<K> {
checkArgument(MotionEvents.isActionUp(e)); checkArgument(MotionEvents.isActionUp(e));
} }
if (!mDetailsLookup.overItemWithSelectionKey(e)) {
if (DEBUG) Log.d(TAG, "Tap not associated w/ model item. Clearing selection.");
mSelectionTracker.clearSelection();
return false;
}
ItemDetails<K> item = mDetailsLookup.getItemDetails(e); ItemDetails<K> item = mDetailsLookup.getItemDetails(e);
// Should really not be null at this point, but... if (item == null || !item.hasSelectionKey()) {
if (item == null) { if (DEBUG) Log.d(TAG, "Tap not associated w/ model item. Clearing selection.");
return false; return mSelectionTracker.clearSelection();
} }
if (mSelectionTracker.hasSelection()) { if (mSelectionTracker.hasSelection()) {
@ -111,6 +109,25 @@ final class TouchInputHandler<K> extends MotionInputHandler<K> {
: mOnItemActivatedListener.onItemActivated(item, e); : mOnItemActivatedListener.onItemActivated(item, e);
} }
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
// Reinterpret an UP event in the double tap event stream as a singleTapUp.
//
// Background: GestureRouter is both an OnGestureListener and a OnDoubleTapListener,
// which allows it to dispatch events based on tooltype to the Touch or Mouse
// input handler.
//
// Turns out the act of instantiating GestureDetector with an OnDoubleTapListener
// signals to it that we want onDoubleTap events rather than a series of individual
// onSingleTapUp events, resulting in some touch input being mishandled
// by TouchInputHandler. See b/161162268 for some supporting details.
//
// There are a variety of ways to work around this. Given long term plans
// to replace GestureDetector (b/159025478), we'll just reroute
// the second UP event to the onSingleTapUp handler.
return MotionEvents.isActionUp(e) && onSingleTapUp(e);
}
@Override @Override
public void onLongPress(@NonNull MotionEvent e) { public void onLongPress(@NonNull MotionEvent e) {
if (DEBUG) { if (DEBUG) {
@ -129,26 +146,31 @@ final class TouchInputHandler<K> extends MotionInputHandler<K> {
return; return;
} }
// Temprary fix to address b/166836317.
mLongPressCallback.run();
if (shouldExtendRange(e)) { if (shouldExtendRange(e)) {
extendSelectionRange(item); extendSelectionRange(item);
mHapticPerformer.run(); mHapticPerformer.run();
} else { return;
if (mSelectionTracker.isSelected(item.getSelectionKey())) { }
// Long press on existing selected item initiates drag/drop.
mOnDragInitiatedListener.onDragInitiated(e); if (mSelectionTracker.isSelected(item.getSelectionKey())) {
mHapticPerformer.run(); // Long press on existing selected item initiates drag/drop.
} else if (mSelectionPredicate.canSetStateForKey(item.getSelectionKey(), true) if (mOnDragInitiatedListener.onDragInitiated(e)) {
&& selectItem(item)) {
// And finally if the item was selected && we can select multiple
// we kick off gesture selection.
// NOTE: isRangeActive should ALWAYS be true at this point, but there have
// been reports indicating that assumption isn't correct. So we explicitly
// check isRangeActive.
if (mSelectionPredicate.canSelectMultiple() && mSelectionTracker.isRangeActive()) {
mGestureStarter.run();
}
mHapticPerformer.run(); mHapticPerformer.run();
} }
} else if (mSelectionPredicate.canSetStateForKey(item.getSelectionKey(), true)
&& selectItem(item)) {
// And finally if the item was selected && we can select multiple
// we kick off gesture selection.
// NOTE: isRangeActive should ALWAYS be true at this point, but there have
// been reports indicating that assumption isn't correct. So we explicitly
// check isRangeActive.
if (mSelectionPredicate.canSelectMultiple() && mSelectionTracker.isRangeActive()) {
mGestureStarter.run();
}
mHapticPerformer.run();
} }
} }
} }

@ -25,12 +25,13 @@ import android.graphics.Point;
import android.graphics.Rect; import android.graphics.Rect;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import androidx.core.view.ViewCompat; import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
/** /**
* Provides auto-scrolling upon request when user's interaction with the application * Provides auto-scrolling upon request when user's interaction with the application
* introduces a natural intent to scroll. Used by BandSelectionHelper and GestureSelectionHelper, * introduces a natural intent to scroll. Used by BandSelectionHelper and GestureSelectionHelper,

@ -0,0 +1,149 @@
diff --git b/app/src/main/java/androidx/recyclerview/selection/DefaultSelectionTracker.java a/app/src/main/java/androidx/recyclerview/selection/DefaultSelectionTracker.java
index 88418ace1d..ffc0f8736a 100644
--- b/app/src/main/java/androidx/recyclerview/selection/DefaultSelectionTracker.java
+++ a/app/src/main/java/androidx/recyclerview/selection/DefaultSelectionTracker.java
@@ -379,6 +379,10 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> implements R
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);
}
@@ -395,7 +399,7 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> implements R
return;
}
- mSelection.clearProvisionalSelection();
+ //mSelection.clearProvisionalSelection();
notifySelectionRefresh();
@@ -611,12 +615,14 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> implements R
@Override
public void onItemRangeInserted(int startPosition, int itemCount) {
- mSelectionTracker.endRange();
+ if (mSelectionTracker.isOverlapping(startPosition, itemCount))
+ mSelectionTracker.endRange();
}
@Override
public void onItemRangeRemoved(int startPosition, int itemCount) {
- mSelectionTracker.endRange();
+ if (mSelectionTracker.isOverlapping(startPosition, itemCount))
+ mSelectionTracker.endRange();
// Since SelectionTracker deals in keys, not positions, we turn
// to the `onDataSetChanged` sledge hammer.
// DefaultSelectionTracker will validate and update it's selection.
@@ -625,7 +631,9 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> implements R
@Override
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
- mSelectionTracker.endRange();
+ if (mSelectionTracker.isOverlapping(fromPosition, itemCount) ||
+ mSelectionTracker.isOverlapping(toPosition, itemCount))
+ mSelectionTracker.endRange();
}
}
}
diff --git b/app/src/main/java/androidx/recyclerview/selection/GestureRouter.java a/app/src/main/java/androidx/recyclerview/selection/GestureRouter.java
index 9f6068f7bf..72e5c948cd 100644
--- b/app/src/main/java/androidx/recyclerview/selection/GestureRouter.java
+++ a/app/src/main/java/androidx/recyclerview/selection/GestureRouter.java
@@ -94,7 +94,28 @@ final class GestureRouter<T extends OnGestureListener & OnDoubleTapListener>
@Override
public void onLongPress(@NonNull MotionEvent e) {
- mDelegates.get(e).onLongPress(e);
+ try {
+ mDelegates.get(e).onLongPress(e);
+ } catch (Throwable ex) {
+ eu.faircode.email.Log.w(ex);
+ /*
+ java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling eu.faircode.email.FixedRecyclerView{239c688b VFED.... ........ 0,0-800,1162 #7f0a04da app:id/rvMessage}, adapter:eu.faircode.email.AdapterMessage@209415c5, layout:eu.faircode.email.FragmentMessages$7@190d7b1a, context:eu.faircode.email.ActivityView@3e8522fb
+ at androidx.recyclerview.widget.RecyclerView.assertNotInLayoutOrScroll(SourceFile:3)
+ at androidx.recyclerview.widget.RecyclerView$RecyclerViewDataObserver.onItemRangeChanged(SourceFile:1)
+ at androidx.recyclerview.widget.RecyclerView$AdapterDataObservable.notifyItemRangeChanged(SourceFile:3)
+ at androidx.recyclerview.widget.RecyclerView$Adapter.notifyItemChanged(SourceFile:2)
+ at androidx.recyclerview.selection.EventBridge$TrackerToAdapterBridge.onItemStateChanged(SourceFile:3)
+ at androidx.recyclerview.selection.DefaultSelectionTracker.notifyItemStateChanged(SourceFile:3)
+ at androidx.recyclerview.selection.DefaultSelectionTracker.select(SourceFile:8)
+ at androidx.recyclerview.selection.MotionInputHandler.selectItem(SourceFile:4)
+ at androidx.recyclerview.selection.TouchInputHandler.onLongPress(SourceFile:10)
+ at androidx.recyclerview.selection.GestureRouter.onLongPress(SourceFile:1)
+ at android.view.GestureDetector.dispatchLongPress(GestureDetector.java:700)
+ at android.view.GestureDetector.access$200(GestureDetector.java:40)
+ at android.view.GestureDetector$GestureHandler.handleMessage(GestureDetector.java:273)
+ at android.os.Handler.dispatchMessage(Handler.java:102)
+ */
+ }
}
@Override
diff --git b/app/src/main/java/androidx/recyclerview/selection/Range.java a/app/src/main/java/androidx/recyclerview/selection/Range.java
index 6a53b1f4fc..dc372bad93 100644
--- b/app/src/main/java/androidx/recyclerview/selection/Range.java
+++ a/app/src/main/java/androidx/recyclerview/selection/Range.java
@@ -170,6 +170,11 @@ 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 b/app/src/main/java/androidx/recyclerview/selection/SelectionTracker.java a/app/src/main/java/androidx/recyclerview/selection/SelectionTracker.java
index 41ddefa9b1..8c86d4adbc 100644
--- b/app/src/main/java/androidx/recyclerview/selection/SelectionTracker.java
+++ a/app/src/main/java/androidx/recyclerview/selection/SelectionTracker.java
@@ -529,7 +529,7 @@ public abstract class SelectionTracker<K> {
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;
// TODO(b/144500333): Remove support for overriding gesture and pointer tooltypes.
private int[] mGestureToolTypes = new int[]{
@@ -675,7 +675,7 @@ public abstract class SelectionTracker<K> {
* @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.
*/
- @Deprecated
+ //@Deprecated
public @NonNull Builder<K> withGestureTooltypes(int @NonNull ... toolTypes) {
Log.w(TAG, "Setting gestureTooltypes is likely to result in unexpected behavior.");
mGestureToolTypes = toolTypes;
@@ -713,7 +713,7 @@ public abstract class SelectionTracker<K> {
* @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.
*/
- @Deprecated
+ //@Deprecated
public @NonNull Builder<K> withPointerTooltypes(int @NonNull ... toolTypes) {
Log.w(TAG, "Setting pointerTooltypes is likely to result in unexpected behavior.");
mPointerToolTypes = toolTypes;
diff --git b/app/src/main/java/androidx/recyclerview/selection/ViewAutoScroller.java a/app/src/main/java/androidx/recyclerview/selection/ViewAutoScroller.java
index 4701bef30c..7b6551f8e2 100644
--- b/app/src/main/java/androidx/recyclerview/selection/ViewAutoScroller.java
+++ a/app/src/main/java/androidx/recyclerview/selection/ViewAutoScroller.java
@@ -108,6 +108,11 @@ final class ViewAutoScroller extends AutoScroller {
if (VERBOSE) Log.v(TAG, "Running in background using event location @ " + mLastLocation);
+ if (mLastLocation == null) {
+ eu.faircode.email.Log.w("ViewAutoScroller.mLastLocation is null");
+ return;
+ }
+
// Compute the number of pixels the pointer's y-coordinate is past the view.
// Negative values mean the pointer is at or before the top of the view, and
// positive values mean that the pointer is at or after the bottom of the view. Note
Loading…
Cancel
Save