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 }