Home | History | Annotate | Download | only in ui
      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      */
     78     public ConversationCheckedSet() {
     79         // Do nothing.
     80     }
     81 
     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      */
     95     public void addObserver(ConversationSetObserver observer) {
     96         synchronized (mLock) {
     97             mObservers.add(observer);
     98         }
     99     }
    100 
    101     /**
    102      * Clear the selected set entirely.
    103      */
    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      */
    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      */
    135     public boolean contains(Conversation conversation) {
    136         synchronized (mLock) {
    137             return containsKey(conversation.id);
    138         }
    139     }
    140 
    141     @Override
    142     public int describeContents() {
    143         return 0;
    144     }
    145 
    146     private void dispatchOnBecomeUnempty(ArrayList<ConversationSetObserver> observers) {
    147         synchronized (mLock) {
    148             for (ConversationSetObserver observer : observers) {
    149                 observer.onSetPopulated(this);
    150             }
    151         }
    152     }
    153 
    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 
    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      */
    176     public boolean isEmpty() {
    177         synchronized (mLock) {
    178             return mInternalMap.isEmpty();
    179         }
    180     }
    181 
    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 */
    197     private void remove(Long id) {
    198         synchronized (mLock) {
    199             removeAll(Collections.singleton(id));
    200         }
    201     }
    202 
    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      */
    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      */
    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      */
    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 */
    260     public Collection<Conversation> values() {
    261         synchronized (mLock) {
    262             return mInternalMap.values();
    263         }
    264     }
    265 
    266     /** @see java.util.HashMap#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      */
    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
    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      */
    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      */
    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
    366     public String toString() {
    367         synchronized (mLock) {
    368             return String.format("%s:%s", super.toString(), mInternalMap);
    369         }
    370     }
    371 }
    372