ConversationSelectionSet.java revision 0a22d4482396f3717b36796e594d5f8e9760d509
1/******************************************************************************* 2 * Copyright (C) 2012 Google Inc. 3 * Licensed to The Android Open Source Project. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 *******************************************************************************/ 17 18package com.android.mail.ui; 19 20import com.google.common.annotations.VisibleForTesting; 21import com.google.common.collect.BiMap; 22import com.google.common.collect.HashBiMap; 23import com.google.common.collect.Lists; 24import com.google.common.collect.Sets; 25 26import android.os.Parcel; 27import android.os.Parcelable; 28 29import com.android.mail.browse.ConversationItemView; 30import com.android.mail.browse.ConversationCursor; 31import com.android.mail.providers.Conversation; 32import com.android.mail.utils.Utils; 33 34import java.util.ArrayList; 35import java.util.Collection; 36import java.util.Collections; 37import java.util.HashMap; 38import java.util.HashSet; 39import java.util.Set; 40 41/** 42 * A simple thread-safe wrapper over a set of conversations representing a 43 * selection set (e.g. in a conversation list). This class dispatches changes 44 * when the set goes empty, and when it becomes unempty. For simplicity, this 45 * class <b>does not allow modifications</b> to the collection in observers when 46 * responding to change events. 47 */ 48public class ConversationSelectionSet implements Parcelable { 49 public static final Parcelable.Creator<ConversationSelectionSet> CREATOR = 50 new Parcelable.Creator<ConversationSelectionSet>() { 51 52 @Override 53 public ConversationSelectionSet createFromParcel(Parcel source) { 54 ConversationSelectionSet result = new ConversationSelectionSet(); 55 Parcelable[] conversations = source.readParcelableArray( 56 Conversation.class.getClassLoader()); 57 for (Parcelable parceled : conversations) { 58 Conversation conversation = (Conversation) parceled; 59 result.put(conversation.id, conversation); 60 } 61 return result; 62 } 63 64 @Override 65 public ConversationSelectionSet[] newArray(int size) { 66 return new ConversationSelectionSet[size]; 67 } 68 }; 69 70 private final Object mLock = new Object(); 71 private final HashMap<Long, Conversation> mInternalMap = 72 new HashMap<Long, Conversation>(); 73 74 /** 75 * Map of conversation IDs to {@link ConversationItemView} objects. The views are <b>not</b> 76 * updated when a new list view object is created on orientation change. 77 */ 78 private final HashMap<Long, ConversationItemView> mInternalViewMap = 79 new HashMap<Long, ConversationItemView>(); 80 private final BiMap<String, Long> mConversationUriToIdMap = HashBiMap.create(); 81 82 @VisibleForTesting 83 final ArrayList<ConversationSetObserver> mObservers = new ArrayList<ConversationSetObserver>(); 84 85 /** 86 * Registers an observer to listen for interesting changes on this set. 87 * 88 * @param observer the observer to register. 89 */ 90 public void addObserver(ConversationSetObserver observer) { 91 synchronized (mLock) { 92 mObservers.add(observer); 93 } 94 } 95 96 /** 97 * Clear the selected set entirely. 98 */ 99 public void clear() { 100 synchronized (mLock) { 101 boolean initiallyNotEmpty = !mInternalMap.isEmpty(); 102 mInternalViewMap.clear(); 103 mInternalMap.clear(); 104 mConversationUriToIdMap.clear(); 105 106 if (mInternalMap.isEmpty() && initiallyNotEmpty) { 107 ArrayList<ConversationSetObserver> observersCopy = Lists.newArrayList(mObservers); 108 dispatchOnChange(observersCopy); 109 dispatchOnEmpty(observersCopy); 110 } 111 } 112 } 113 114 /** 115 * Returns true if the given key exists in the conversation selection set. This assumes 116 * the internal representation holds conversation.id values. 117 * @param key the id of the conversation 118 * @return true if the key exists in this selected set. 119 */ 120 public boolean containsKey(Long key) { 121 synchronized (mLock) { 122 return mInternalMap.containsKey(key); 123 } 124 } 125 126 /** 127 * Returns true if the given conversation is stored in the selection set. 128 * @param conversation 129 * @return true if the conversation exists in the selected set. 130 */ 131 public boolean contains(Conversation conversation) { 132 synchronized (mLock) { 133 return containsKey(conversation.id); 134 } 135 } 136 137 @Override 138 public int describeContents() { 139 return 0; 140 } 141 142 private void dispatchOnBecomeUnempty(ArrayList<ConversationSetObserver> observers) { 143 synchronized (mLock) { 144 for (ConversationSetObserver observer : observers) { 145 observer.onSetPopulated(this); 146 } 147 } 148 } 149 150 private void dispatchOnChange(ArrayList<ConversationSetObserver> observers) { 151 synchronized (mLock) { 152 // Copy observers so that they may unregister themselves as listeners on 153 // event handling. 154 for (ConversationSetObserver observer : observers) { 155 observer.onSetChanged(this); 156 } 157 } 158 } 159 160 private void dispatchOnEmpty(ArrayList<ConversationSetObserver> observers) { 161 synchronized (mLock) { 162 for (ConversationSetObserver observer : observers) { 163 observer.onSetEmpty(); 164 } 165 } 166 } 167 168 /** 169 * Is this conversation set empty? 170 * @return true if the conversation selection set is empty. False otherwise. 171 */ 172 public boolean isEmpty() { 173 synchronized (mLock) { 174 return mInternalMap.isEmpty(); 175 } 176 } 177 178 private void put(Long id, Conversation info) { 179 synchronized (mLock) { 180 final boolean initiallyEmpty = mInternalMap.isEmpty(); 181 mInternalMap.put(id, info); 182 // Fill out the view map with null. The sizes will match, but 183 // we won't have any views available yet to store. 184 mInternalViewMap.put(id, null); 185 mConversationUriToIdMap.put(info.uri.toString(), id); 186 187 final ArrayList<ConversationSetObserver> observersCopy = Lists.newArrayList(mObservers); 188 dispatchOnChange(observersCopy); 189 if (initiallyEmpty) { 190 dispatchOnBecomeUnempty(observersCopy); 191 } 192 } 193 } 194 195 /** @see java.util.HashMap#put */ 196 private void put(Long id, ConversationItemView info) { 197 synchronized (mLock) { 198 boolean initiallyEmpty = mInternalMap.isEmpty(); 199 mInternalViewMap.put(id, info); 200 mInternalMap.put(id, info.mHeader.conversation); 201 mConversationUriToIdMap.put(info.mHeader.conversation.uri.toString(), id); 202 203 final ArrayList<ConversationSetObserver> observersCopy = Lists.newArrayList(mObservers); 204 dispatchOnChange(observersCopy); 205 if (initiallyEmpty) { 206 dispatchOnBecomeUnempty(observersCopy); 207 } 208 } 209 } 210 211 /** @see java.util.HashMap#remove */ 212 private void remove(Long id) { 213 synchronized (mLock) { 214 removeAll(Collections.singleton(id)); 215 } 216 } 217 218 private void removeAll(Collection<Long> ids) { 219 synchronized (mLock) { 220 final boolean initiallyNotEmpty = !mInternalMap.isEmpty(); 221 222 final BiMap<Long, String> inverseMap = mConversationUriToIdMap.inverse(); 223 224 for (Long id : ids) { 225 mInternalViewMap.remove(id); 226 mInternalMap.remove(id); 227 inverseMap.remove(id); 228 } 229 230 ArrayList<ConversationSetObserver> observersCopy = Lists.newArrayList(mObservers); 231 dispatchOnChange(observersCopy); 232 if (mInternalMap.isEmpty() && initiallyNotEmpty) { 233 dispatchOnEmpty(observersCopy); 234 } 235 } 236 } 237 238 /** 239 * Unregisters an observer for change events. 240 * 241 * @param observer the observer to unregister. 242 */ 243 public void removeObserver(ConversationSetObserver observer) { 244 synchronized (mLock) { 245 mObservers.remove(observer); 246 } 247 } 248 249 /** 250 * Returns the number of conversations that are currently selected 251 * @return the number of selected conversations. 252 */ 253 public int size() { 254 synchronized (mLock) { 255 return mInternalMap.size(); 256 } 257 } 258 259 /** 260 * Toggles the existence of the given conversation in the selection set. If the conversation is 261 * currently selected, it is deselected. If it doesn't exist in the selection set, then it is 262 * selected. If you are certain that you are deselecting a conversation (you have verified 263 * that {@link #contains(Conversation)} or {@link #containsKey(Long)} are true), then you 264 * may pass a null {@link ConversationItemView}. 265 * @param conversation 266 */ 267 public void toggle(ConversationItemView view, Conversation conversation) { 268 long conversationId = conversation.id; 269 if (containsKey(conversationId)) { 270 // We must not do anything with view here. 271 remove(conversationId); 272 } else { 273 put(conversationId, view); 274 } 275 } 276 277 /** @see java.util.HashMap#values */ 278 public Collection<Conversation> values() { 279 synchronized (mLock) { 280 return mInternalMap.values(); 281 } 282 } 283 284 /** @see java.util.HashMap#keySet() */ 285 public Set<Long> keySet() { 286 synchronized (mLock) { 287 return mInternalMap.keySet(); 288 } 289 } 290 291 /** 292 * Puts all conversations given in the input argument into the selection set. If there are 293 * any listeners they are notified once after adding <em>all</em> conversations to the selection 294 * set. 295 * @see java.util.HashMap#putAll(java.util.Map) 296 */ 297 public void putAll(ConversationSelectionSet other) { 298 if (other == null) { 299 return; 300 } 301 302 final boolean initiallyEmpty = mInternalMap.isEmpty(); 303 mInternalMap.putAll(other.mInternalMap); 304 305 final Set<Long> keys = other.mInternalMap.keySet(); 306 for (Long key : keys) { 307 // Fill out the view map with null. The sizes will match, but 308 // we won't have any views available yet to store. 309 mInternalViewMap.put(key, null); 310 } 311 312 ArrayList<ConversationSetObserver> observersCopy = Lists.newArrayList(mObservers); 313 dispatchOnChange(observersCopy); 314 if (initiallyEmpty) { 315 dispatchOnBecomeUnempty(observersCopy); 316 } 317 } 318 319 @Override 320 public void writeToParcel(Parcel dest, int flags) { 321 Conversation[] values = values().toArray(new Conversation[size()]); 322 dest.writeParcelableArray(values, flags); 323 } 324 325 public Collection<ConversationItemView> views() { 326 return mInternalViewMap.values(); 327 } 328 329 /** 330 * @param deletedRows an arraylist of conversation IDs which have been deleted. 331 */ 332 public void delete(ArrayList<Integer> deletedRows) { 333 for (long id : deletedRows) { 334 remove(id); 335 } 336 } 337 338 /** 339 * Iterates through a cursor of conversations and ensures that the current set is present 340 * within the result set denoted by the cursor. Any conversations not foun in the result set 341 * is removed from the collection. 342 */ 343 public void validateAgainstCursor(ConversationCursor cursor) { 344 synchronized (mLock) { 345 if (isEmpty()) { 346 return; 347 } 348 349 if (cursor == null) { 350 clear(); 351 return; 352 } 353 354 // First ask the ConversationCursor for the list of conversations that have been deleted 355 final Set<String> deletedConversations = cursor.getDeletedItems(); 356 // For each of the uris in the deleted set, add the conversation id to the 357 // itemsToRemoveFromBatch set. 358 final Set<Long> itemsToRemoveFromBatch = Sets.newHashSet(); 359 for (String conversationUri : deletedConversations) { 360 final Long conversationId = mConversationUriToIdMap.get(conversationUri); 361 if (conversationId != null) { 362 itemsToRemoveFromBatch.add(conversationId); 363 } 364 } 365 366 // Get the set of the items that had been in the batch 367 final Set<Long> batchConversationToCheck = new HashSet<Long>(keySet()); 368 369 // Remove all of the items that we know are missing. This will leave the items where 370 // we need to check for existence in the cursor 371 batchConversationToCheck.removeAll(itemsToRemoveFromBatch); 372 373 // While there are items to check, remove all items that are still in the cursor. 374 final Set<Long> cursorConversationIds = cursor.getConversationIds(); 375 while (!batchConversationToCheck.isEmpty() && cursorConversationIds != null) { 376 batchConversationToCheck.removeAll(cursorConversationIds); 377 } 378 379 // At this point any of the item that are remaining in the batchConversationToCheck set 380 // are to be removed from the selected conversation set 381 itemsToRemoveFromBatch.addAll(batchConversationToCheck); 382 383 removeAll(itemsToRemoveFromBatch); 384 } 385 } 386 387 @Override 388 public String toString() { 389 synchronized (mLock) { 390 return String.format("%s:%s", super.toString(), mInternalMap); 391 } 392 } 393} 394