• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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 package com.android.documentsui.selection;
17 
18 import static android.support.v4.util.Preconditions.checkArgument;
19 
20 import android.os.Parcel;
21 import android.os.Parcelable;
22 import android.support.annotation.VisibleForTesting;
23 
24 import java.util.ArrayList;
25 import java.util.Collection;
26 import java.util.HashMap;
27 import java.util.HashSet;
28 import java.util.Iterator;
29 import java.util.Map;
30 import java.util.Set;
31 
32 import javax.annotation.Nullable;
33 
34 /**
35  * Object representing the current selection and provisional selection. Provides read only public
36  * access, and private write access.
37  * <p>
38  * This class tracks selected items by managing two sets:
39  *
40  * <li>primary selection
41  *
42  * Primary selection consists of items tapped by a user or by lassoed by band select operation.
43  *
44  * <li>provisional selection
45  *
46  * Provisional selections are selections which have been temporarily created
47  * by an in-progress band select or gesture selection. Once the user releases the mouse button
48  * or lifts their finger the corresponding provisional selection should be converted into
49  * primary selection.
50  *
51  * <p>The total selection is the combination of
52  * both the core selection and the provisional selection. Tracking both separately is necessary to
53  * ensure that items in the core selection are not "erased" from the core selection when they
54  * are temporarily included in a secondary selection (like band selection).
55  */
56 public class Selection implements Iterable<String>, Parcelable {
57 
58     final Set<String> mSelection;
59     final Set<String> mProvisionalSelection;
60 
Selection()61     public Selection() {
62         mSelection = new HashSet<>();
63         mProvisionalSelection = new HashSet<>();
64     }
65 
66     /**
67      * Used by CREATOR.
68      */
Selection(Set<String> selection)69     private Selection(Set<String> selection) {
70         mSelection = selection;
71         mProvisionalSelection = new HashSet<>();
72     }
73 
74     /**
75      * @param id
76      * @return true if the position is currently selected.
77      */
contains(@ullable String id)78     public boolean contains(@Nullable String id) {
79         return mSelection.contains(id) || mProvisionalSelection.contains(id);
80     }
81 
82     /**
83      * Returns an {@link Iterator} that iterators over the selection, *excluding*
84      * any provisional selection.
85      *
86      * {@inheritDoc}
87      */
88     @Override
iterator()89     public Iterator<String> iterator() {
90         return mSelection.iterator();
91     }
92 
93     /**
94      * @return size of the selection including both final and provisional selected items.
95      */
size()96     public int size() {
97         return mSelection.size() + mProvisionalSelection.size();
98     }
99 
100     /**
101      * @return true if the selection is empty.
102      */
isEmpty()103     public boolean isEmpty() {
104         return mSelection.isEmpty() && mProvisionalSelection.isEmpty();
105     }
106 
107     /**
108      * Sets the provisional selection, which is a temporary selection that can be saved,
109      * canceled, or adjusted at a later time. When a new provision selection is applied, the old
110      * one (if it exists) is abandoned.
111      * @return Map of ids added or removed. Added ids have a value of true, removed are false.
112      */
setProvisionalSelection(Set<String> newSelection)113     Map<String, Boolean> setProvisionalSelection(Set<String> newSelection) {
114         Map<String, Boolean> delta = new HashMap<>();
115 
116         for (String id: mProvisionalSelection) {
117             // Mark each item that used to be in the provisional selection
118             // but is not in the new provisional selection.
119             if (!newSelection.contains(id) && !mSelection.contains(id)) {
120                 delta.put(id, false);
121             }
122         }
123 
124         for (String id: mSelection) {
125             // Mark each item that used to be in the selection but is unsaved and not in the new
126             // provisional selection.
127             if (!newSelection.contains(id)) {
128                 delta.put(id, false);
129             }
130         }
131 
132         for (String id: newSelection) {
133             // Mark each item that was not previously in the selection but is in the new
134             // provisional selection.
135             if (!mSelection.contains(id) && !mProvisionalSelection.contains(id)) {
136                 delta.put(id, true);
137             }
138         }
139 
140         // Now, iterate through the changes and actually add/remove them to/from the current
141         // selection. This could not be done in the previous loops because changing the size of
142         // the selection mid-iteration changes iteration order erroneously.
143         for (Map.Entry<String, Boolean> entry: delta.entrySet()) {
144             String id = entry.getKey();
145             if (entry.getValue()) {
146                 mProvisionalSelection.add(id);
147             } else {
148                 mProvisionalSelection.remove(id);
149             }
150         }
151 
152         return delta;
153     }
154 
155     /**
156      * Saves the existing provisional selection. Once the provisional selection is saved,
157      * subsequent provisional selections which are different from this existing one cannot
158      * cause items in this existing provisional selection to become deselected.
159      */
160     @VisibleForTesting
mergeProvisionalSelection()161     protected void mergeProvisionalSelection() {
162         mSelection.addAll(mProvisionalSelection);
163         mProvisionalSelection.clear();
164     }
165 
166     /**
167      * Abandons the existing provisional selection so that all items provisionally selected are
168      * now deselected.
169      */
170     @VisibleForTesting
clearProvisionalSelection()171     void clearProvisionalSelection() {
172         mProvisionalSelection.clear();
173     }
174 
175     /**
176      * Adds a new item to the primary selection.
177      *
178      * @return true if the operation resulted in a modification to the selection.
179      */
add(String id)180     boolean add(String id) {
181         if (mSelection.contains(id)) {
182             return false;
183         }
184 
185         mSelection.add(id);
186         return true;
187     }
188 
189     /**
190      * Removes an item from the primary selection.
191      *
192      * @return true if the operation resulted in a modification to the selection.
193      */
remove(String id)194     boolean remove(String id) {
195         if (!mSelection.contains(id)) {
196             return false;
197         }
198 
199         mSelection.remove(id);
200         return true;
201     }
202 
203     /**
204      * Clears the primary selection. The provisional selection, if any, is unaffected.
205      */
clear()206     void clear() {
207         mSelection.clear();
208     }
209 
210     /**
211      * Trims this selection to be the intersection of itself and {@code ids}.
212      */
intersect(Collection<String> ids)213     void intersect(Collection<String> ids) {
214         checkArgument(ids != null);
215 
216         mSelection.retainAll(ids);
217         mProvisionalSelection.retainAll(ids);
218     }
219 
220     /**
221      * Clones primary and provisional selection from supplied {@link Selection}.
222      * Does not copy active range data.
223      */
224     @VisibleForTesting
copyFrom(Selection source)225     void copyFrom(Selection source) {
226         mSelection.clear();
227         mSelection.addAll(source.mSelection);
228 
229         mProvisionalSelection.clear();
230         mProvisionalSelection.addAll(source.mProvisionalSelection);
231     }
232 
233     @Override
toString()234     public String toString() {
235         if (size() <= 0) {
236             return "size=0, items=[]";
237         }
238 
239         StringBuilder buffer = new StringBuilder(size() * 28);
240         buffer.append("Selection{")
241             .append("primary{size=" + mSelection.size())
242             .append(", entries=" + mSelection)
243             .append("}, provisional{size=" + mProvisionalSelection.size())
244             .append(", entries=" + mProvisionalSelection)
245             .append("}}");
246         return buffer.toString();
247     }
248 
249     @Override
hashCode()250     public int hashCode() {
251         return mSelection.hashCode() ^ mProvisionalSelection.hashCode();
252     }
253 
254     @Override
equals(Object other)255     public boolean equals(Object other) {
256         if (this == other) {
257             return true;
258         }
259 
260         return other instanceof Selection
261             ? equals((Selection) other)
262             : false;
263     }
264 
equals(Selection other)265     private boolean equals(Selection other) {
266         return mSelection.equals(((Selection) other).mSelection) &&
267                 mProvisionalSelection.equals(((Selection) other).mProvisionalSelection);
268     }
269 
270     @Override
describeContents()271     public int describeContents() {
272         return 0;
273     }
274 
275     @Override
writeToParcel(Parcel dest, int flags)276     public void writeToParcel(Parcel dest, int flags) {
277         dest.writeStringList(new ArrayList<>(mSelection));
278         // We don't include provisional selection since it is
279         // typically coupled to some other runtime state (like a band).
280     }
281 
282     public static final ClassLoaderCreator<Selection> CREATOR =
283             new ClassLoaderCreator<Selection>() {
284         @Override
285         public Selection createFromParcel(Parcel in) {
286             return createFromParcel(in, null);
287         }
288 
289         @Override
290         public Selection createFromParcel(Parcel in, ClassLoader loader) {
291             ArrayList<String> selected = new ArrayList<>();
292             in.readStringList(selected);
293 
294             return new Selection(new HashSet<>(selected));
295         }
296 
297         @Override
298         public Selection[] newArray(int size) {
299             return new Selection[size];
300         }
301     };
302 }