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