Home | History | Annotate | Download | only in browse
      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.browse;
     19 
     20 import android.content.Context;
     21 import android.graphics.Bitmap;
     22 import android.text.SpannableString;
     23 import android.text.SpannableStringBuilder;
     24 import android.text.StaticLayout;
     25 import android.text.TextUtils;
     26 import android.text.format.DateUtils;
     27 import android.util.LruCache;
     28 import android.util.Pair;
     29 
     30 import com.android.mail.R;
     31 import com.android.mail.providers.Conversation;
     32 import com.android.mail.providers.Folder;
     33 import com.android.mail.providers.ParticipantInfo;
     34 import com.android.mail.providers.UIProvider;
     35 import com.android.mail.utils.FolderUri;
     36 import com.google.common.annotations.VisibleForTesting;
     37 import com.google.common.base.Objects;
     38 
     39 import java.util.ArrayList;
     40 import java.util.List;
     41 
     42 /**
     43  * This is the view model for the conversation header. It includes all the
     44  * information needed to layout a conversation header view. Each view model is
     45  * associated with a conversation and is cached to improve the relayout time.
     46  */
     47 public class ConversationItemViewModel {
     48     private static final int MAX_CACHE_SIZE = 100;
     49 
     50     @VisibleForTesting
     51     static LruCache<Pair<String, Long>, ConversationItemViewModel> sConversationHeaderMap
     52         = new LruCache<Pair<String, Long>, ConversationItemViewModel>(MAX_CACHE_SIZE);
     53 
     54     /**
     55      * The Folder associated with the cache of models.
     56      */
     57     private static Folder sCachedModelsFolder;
     58 
     59     // The hashcode used to detect if the conversation has changed.
     60     private int mDataHashCode;
     61     private int mLayoutHashCode;
     62 
     63     // Unread
     64     public boolean unread;
     65 
     66     // Date
     67     CharSequence dateText;
     68     public boolean showDateText = true;
     69 
     70     // Personal level
     71     Bitmap personalLevelBitmap;
     72 
     73     public Bitmap infoIcon;
     74 
     75     public String badgeText;
     76 
     77     public int insetPadding = 0;
     78 
     79     // Paperclip
     80     Bitmap paperclip;
     81 
     82     /** If <code>true</code>, we will not apply any formatting to {@link #sendersText}. */
     83     public boolean preserveSendersText = false;
     84 
     85     // Senders
     86     public String sendersText;
     87 
     88     SpannableStringBuilder sendersDisplayText;
     89     StaticLayout sendersDisplayLayout;
     90 
     91     boolean hasDraftMessage;
     92 
     93     // View Width
     94     public int viewWidth;
     95 
     96     // Standard scaled dimen used to detect if the scale of text has changed.
     97     @Deprecated
     98     public int standardScaledDimen;
     99 
    100     public long maxMessageId;
    101 
    102     public int gadgetMode;
    103 
    104     public Conversation conversation;
    105 
    106     public ConversationItemView.ConversationItemFolderDisplayer folderDisplayer;
    107 
    108     public boolean hasBeenForwarded;
    109 
    110     public boolean hasBeenRepliedTo;
    111 
    112     public boolean isInvite;
    113 
    114     public SpannableStringBuilder messageInfoString;
    115 
    116     public int styledMessageInfoStringOffset;
    117 
    118     private String mContentDescription;
    119 
    120     /**
    121      * The email address and name of the sender whose avatar will be drawn as a conversation icon.
    122      */
    123     public final SenderAvatarModel mSenderAvatarModel = new SenderAvatarModel();
    124 
    125     /**
    126      * Display names corresponding to the email address for the senders/recipients that will be
    127      * displayed on the top line.
    128      */
    129     public final ArrayList<String> displayableNames = new ArrayList<>();
    130 
    131     /**
    132      * A styled version of the {@link #displayableNames} to be displayed on the top line.
    133      */
    134     public final ArrayList<SpannableString> styledNames = new ArrayList<>();
    135 
    136     /**
    137      * Returns the view model for a conversation. If the model doesn't exist for this conversation
    138      * null is returned. Note: this should only be called from the UI thread.
    139      *
    140      * @param account the account contains this conversation
    141      * @param conversationId the Id of this conversation
    142      * @return the view model for this conversation, or null
    143      */
    144     @VisibleForTesting
    145     static ConversationItemViewModel forConversationIdOrNull(String account, long conversationId) {
    146         final Pair<String, Long> key = new Pair<String, Long>(account, conversationId);
    147         synchronized(sConversationHeaderMap) {
    148             return sConversationHeaderMap.get(key);
    149         }
    150     }
    151 
    152     static ConversationItemViewModel forConversation(String account, Conversation conv) {
    153         ConversationItemViewModel header = ConversationItemViewModel.forConversationId(account,
    154                 conv.id);
    155         header.conversation = conv;
    156         header.unread = !conv.read;
    157         header.hasBeenForwarded =
    158                 (conv.convFlags & UIProvider.ConversationFlags.FORWARDED)
    159                 == UIProvider.ConversationFlags.FORWARDED;
    160         header.hasBeenRepliedTo =
    161                 (conv.convFlags & UIProvider.ConversationFlags.REPLIED)
    162                 == UIProvider.ConversationFlags.REPLIED;
    163         header.isInvite =
    164                 (conv.convFlags & UIProvider.ConversationFlags.CALENDAR_INVITE)
    165                 == UIProvider.ConversationFlags.CALENDAR_INVITE;
    166         return header;
    167     }
    168 
    169     /**
    170      * Returns the view model for a conversation. If this is the first time
    171      * call, a new view model will be returned. Note: this should only be called
    172      * from the UI thread.
    173      *
    174      * @param account the account contains this conversation
    175      * @param conversationId the Id of this conversation
    176      * @return the view model for this conversation
    177      */
    178     static ConversationItemViewModel forConversationId(String account, long conversationId) {
    179         synchronized(sConversationHeaderMap) {
    180             ConversationItemViewModel header =
    181                     forConversationIdOrNull(account, conversationId);
    182             if (header == null) {
    183                 final Pair<String, Long> key = new Pair<String, Long>(account, conversationId);
    184                 header = new ConversationItemViewModel();
    185                 sConversationHeaderMap.put(key, header);
    186             }
    187             return header;
    188         }
    189     }
    190 
    191     /**
    192      * Returns the hashcode to compare if the data in the header is valid.
    193      */
    194     private static int getHashCode(CharSequence dateText, Object convInfo,
    195             List<Folder> rawFolders, boolean starred, boolean read, int priority,
    196             int sendingState) {
    197         if (dateText == null) {
    198             return -1;
    199         }
    200         return Objects.hashCode(convInfo, dateText, rawFolders, starred, read, priority,
    201                 sendingState);
    202     }
    203 
    204     /**
    205      * Returns the layout hashcode to compare to see if the layout state has changed.
    206      */
    207     private int getLayoutHashCode() {
    208         return Objects.hashCode(mDataHashCode, viewWidth, standardScaledDimen, gadgetMode);
    209     }
    210 
    211     /**
    212      * Marks this header as having valid data and layout.
    213      */
    214     void validate() {
    215         mDataHashCode = getHashCode(dateText,
    216                 conversation.conversationInfo, conversation.getRawFolders(), conversation.starred,
    217                 conversation.read, conversation.priority, conversation.sendingState);
    218         mLayoutHashCode = getLayoutHashCode();
    219     }
    220 
    221     /**
    222      * Returns if the data in this model is valid.
    223      */
    224     boolean isDataValid() {
    225         return mDataHashCode == getHashCode(dateText,
    226                 conversation.conversationInfo, conversation.getRawFolders(), conversation.starred,
    227                 conversation.read, conversation.priority, conversation.sendingState);
    228     }
    229 
    230     /**
    231      * Returns if the layout in this model is valid.
    232      */
    233     boolean isLayoutValid() {
    234         return isDataValid() && mLayoutHashCode == getLayoutHashCode();
    235     }
    236 
    237     /**
    238      * Reset the content description; enough content has changed that we need to
    239      * regenerate it.
    240      */
    241     public void resetContentDescription() {
    242         mContentDescription = null;
    243     }
    244 
    245     /**
    246      * Get conversation information to use for accessibility.
    247      */
    248     public CharSequence getContentDescription(Context context, boolean showToHeader,
    249             String foldersDesc) {
    250         if (mContentDescription == null) {
    251             // If any are unread, get the first unread sender.
    252             // If all are unread, get the first sender.
    253             // If all are read, get the last sender.
    254             String participant = "";
    255             String lastParticipant = "";
    256             int last = conversation.conversationInfo.participantInfos != null ?
    257                     conversation.conversationInfo.participantInfos.size() - 1 : -1;
    258             if (last != -1) {
    259                 lastParticipant = conversation.conversationInfo.participantInfos.get(last).name;
    260             }
    261             if (conversation.read) {
    262                 participant = TextUtils.isEmpty(lastParticipant) ?
    263                         SendersView.getMe(showToHeader /* useObjectMe */) : lastParticipant;
    264             } else {
    265                 ParticipantInfo firstUnread = null;
    266                 for (ParticipantInfo p : conversation.conversationInfo.participantInfos) {
    267                     if (!p.readConversation) {
    268                         firstUnread = p;
    269                         break;
    270                     }
    271                 }
    272                 if (firstUnread != null) {
    273                     participant = TextUtils.isEmpty(firstUnread.name) ?
    274                             SendersView.getMe(showToHeader /* useObjectMe */) : firstUnread.name;
    275                 }
    276             }
    277             if (TextUtils.isEmpty(participant)) {
    278                 // Just take the last sender
    279                 participant = lastParticipant;
    280             }
    281 
    282             // the toHeader should read "To: " if requested
    283             String toHeader = "";
    284             if (showToHeader && !TextUtils.isEmpty(participant)) {
    285                 toHeader = SendersView.getFormattedToHeader().toString();
    286             }
    287 
    288             boolean isToday = DateUtils.isToday(conversation.dateMs);
    289             String date = DateUtils.getRelativeTimeSpanString(context, conversation.dateMs)
    290                     .toString();
    291             String readString = context.getString(
    292                     conversation.read ? R.string.read_string : R.string.unread_string);
    293             final int res;
    294             if (foldersDesc == null) {
    295                 res = isToday ? R.string.content_description_today : R.string.content_description;
    296             } else {
    297                 res = isToday ? R.string.content_description_today_with_folders :
    298                         R.string.content_description_with_folders;
    299             }
    300             mContentDescription = context.getString(res, toHeader, participant,
    301                     conversation.subject, conversation.getSnippet(), date, readString,
    302                     foldersDesc);
    303         }
    304         return mContentDescription;
    305     }
    306 
    307     /**
    308      * Clear cached header model objects when accessibility changes.
    309      */
    310 
    311     public static void onAccessibilityUpdated() {
    312         sConversationHeaderMap.evictAll();
    313     }
    314 
    315     /**
    316      * Clear cached header model objects when the folder changes.
    317      */
    318     public static void onFolderUpdated(Folder folder) {
    319         final FolderUri old = sCachedModelsFolder != null
    320                 ? sCachedModelsFolder.folderUri : FolderUri.EMPTY;
    321         final FolderUri newUri = folder != null ? folder.folderUri : FolderUri.EMPTY;
    322         if (!old.equals(newUri)) {
    323             sCachedModelsFolder = folder;
    324             sConversationHeaderMap.evictAll();
    325         }
    326     }
    327 
    328     /**
    329      * This mutable model stores the name and email address of the sender for whom an avatar will
    330      * be drawn as the conversation icon.
    331      */
    332     public static final class SenderAvatarModel {
    333         private String mEmailAddress;
    334         private String mName;
    335 
    336         public String getEmailAddress() {
    337             return mEmailAddress;
    338         }
    339 
    340         public String getName() {
    341             return mName;
    342         }
    343 
    344         /**
    345          * Removes the name and email address of the participant of this avatar.
    346          */
    347         public void clear() {
    348             mName = null;
    349             mEmailAddress = null;
    350         }
    351 
    352         /**
    353          * @param name the name of the participant of this avatar
    354          * @param emailAddress the email address of the participant of this avatar; may not be null
    355          */
    356         public void populate(String name, String emailAddress) {
    357             if (TextUtils.isEmpty(emailAddress)) {
    358                 throw new IllegalArgumentException("email address may not be null or empty");
    359             }
    360 
    361             mName = name;
    362             mEmailAddress = emailAddress;
    363         }
    364 
    365         /**
    366          * @return <tt>true</tt> if this model does not yet contain enough data to produce an
    367          *      avatar image; <tt>false</tt> otherwise
    368          */
    369         public boolean isNotPopulated() {
    370             return TextUtils.isEmpty(mEmailAddress);
    371         }
    372     }
    373 }
    374