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 }