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