1 /*
2  * Copyright 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.recyclerview.selection;
18 
19 import static androidx.core.util.Preconditions.checkArgument;
20 import static androidx.recyclerview.selection.Shared.DEBUG;
21 import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
22 
23 import android.util.Log;
24 
25 import androidx.annotation.IntDef;
26 
27 import org.jspecify.annotations.NonNull;
28 
29 import java.lang.annotation.Retention;
30 import java.lang.annotation.RetentionPolicy;
31 
32 /**
33  * Class providing support for managing range selections.
34  */
35 final class Range {
36 
37     static final int TYPE_PRIMARY = 0;
38 
39     /**
40      * "Provisional" selection represents a overlay on the primary selection. A provisional
41      * selection maybe be eventually added to the primary selection, or it may be abandoned.
42      *
43      * <p>
44      * E.g. BandSelectionHelper creates a provisional selection while a user is actively
45      * selecting items with a band. GestureSelectionHelper creates a provisional selection
46      * while a user is active selecting via gesture.
47      *
48      * <p>
49      * Provisionally selected items are considered to be selected in
50      * {@link Selection#contains(String)} and related methods. A provisional may be abandoned or
51      * merged into the promary selection.
52      *
53      * <p>
54      * A provisional selection may intersect with the primary selection, however clearing the
55      * provisional selection will not affect the primary selection where the two may intersect.
56      */
57     static final int TYPE_PROVISIONAL = 1;
58 
59     @IntDef({
60             TYPE_PRIMARY,
61             TYPE_PROVISIONAL
62     })
63     @Retention(RetentionPolicy.SOURCE)
64     @interface RangeType {
65     }
66 
67     private static final String TAG = "Range";
68 
69     private final Callbacks mCallbacks;
70     private final int mBegin;
71     private int mEnd = NO_POSITION;
72 
73     /**
74      * Creates a new range anchored at {@code position}.
75      */
Range(int position, @NonNull Callbacks callbacks)76     Range(int position, @NonNull Callbacks callbacks) {
77         mBegin = position;
78         mCallbacks = callbacks;
79         if (DEBUG) Log.d(TAG, "Creating new Range anchored @ " + position);
80     }
81 
extendRange(int position, @RangeType int type)82     void extendRange(int position, @RangeType int type) {
83         checkArgument(position != NO_POSITION, "Position cannot be NO_POSITION.");
84 
85         if (mEnd == NO_POSITION || mEnd == mBegin) {
86             // Reset mEnd so it can be established in establishRange.
87             mEnd = NO_POSITION;
88             establishRange(position, type);
89         } else {
90             reviseRange(position, type);
91         }
92     }
93 
establishRange(int position, @RangeType int type)94     private void establishRange(int position, @RangeType int type) {
95         checkArgument(mEnd == NO_POSITION, "End has already been set.");
96 
97         mEnd = position;
98 
99         if (position > mBegin) {
100             if (DEBUG) log(type, "Establishing initial range at @ " + position);
101             updateRange(mBegin + 1, position, true, type);
102         } else if (position < mBegin) {
103             if (DEBUG) log(type, "Establishing initial range at @ " + position);
104             updateRange(position, mBegin - 1, true, type);
105         }
106     }
107 
reviseRange(int position, @RangeType int type)108     private void reviseRange(int position, @RangeType int type) {
109         checkArgument(mEnd != NO_POSITION, "End must already be set.");
110         checkArgument(mBegin != mEnd, "Beging and end point to same position.");
111 
112         if (position == mEnd) {
113             if (DEBUG) log(type, "Ignoring no-op revision for range @ " + position);
114         }
115 
116         if (mEnd > mBegin) {
117             reviseAscending(position, type);
118         } else if (mEnd < mBegin) {
119             reviseDescending(position, type);
120         }
121         // the "else" case is covered by checkArgument at beginning of method.
122 
123         mEnd = position;
124     }
125 
126     /**
127      * Updates an existing ascending selection.
128      */
reviseAscending(int position, @RangeType int type)129     private void reviseAscending(int position, @RangeType int type) {
130         if (DEBUG) log(type, "*ascending* Revising range @ " + position);
131 
132         if (position < mEnd) {
133             if (position < mBegin) {
134                 updateRange(mBegin + 1, mEnd, false, type);
135                 updateRange(position, mBegin - 1, true, type);
136             } else {
137                 updateRange(position + 1, mEnd, false, type);
138             }
139         } else if (position > mEnd) {   // Extending the range...
140             updateRange(mEnd + 1, position, true, type);
141         }
142     }
143 
reviseDescending(int position, @RangeType int type)144     private void reviseDescending(int position, @RangeType int type) {
145         if (DEBUG) log(type, "*descending* Revising range @ " + position);
146 
147         if (position > mEnd) {
148             if (position > mBegin) {
149                 updateRange(mEnd, mBegin - 1, false, type);
150                 updateRange(mBegin + 1, position, true, type);
151             } else {
152                 updateRange(mEnd, position - 1, false, type);
153             }
154         } else if (position < mEnd) {   // Extending the range...
155             updateRange(position, mEnd - 1, true, type);
156         }
157     }
158 
159     /**
160      * Try to set selection state for all elements in range. Not that callbacks can cancel
161      * selection of specific items, so some or even all items may not reflect the desired state
162      * after the update is complete.
163      *
164      * @param begin    Adapter position for range start (inclusive).
165      * @param end      Adapter position for range end (inclusive).
166      * @param selected New selection state.
167      */
updateRange( int begin, int end, boolean selected, @RangeType int type)168     private void updateRange(
169             int begin, int end, boolean selected, @RangeType int type) {
170         mCallbacks.updateForRange(begin, end, selected, type);
171     }
172 
173     @Override
toString()174     public String toString() {
175         return "Range{begin=" + mBegin + ", end=" + mEnd + "}";
176     }
177 
log(@angeType int type, String message)178     private void log(@RangeType int type, String message) {
179         String opType = type == TYPE_PRIMARY ? "PRIMARY" : "PROVISIONAL";
180         Log.d(TAG, String.valueOf(this) + ": " + message + " (" + opType + ")");
181     }
182 
183     /*
184      * @see {@link DefaultSelectionTracker#updateForRange(int, int , boolean, int)}.
185      */
186     abstract static class Callbacks {
updateForRange( int begin, int end, boolean selected, @RangeType int type)187         abstract void updateForRange(
188                 int begin, int end, boolean selected, @RangeType int type);
189     }
190 }
191