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