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