• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
forConversationIdOrNull(String account, long conversationId)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 
forConversation(String account, Conversation conv)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      */
forConversationId(String account, long conversationId)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      */
getHashCode(CharSequence dateText, Object convInfo, List<Folder> rawFolders, boolean starred, boolean read, int priority, int sendingState)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      */
getLayoutHashCode()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      */
validate()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      */
isDataValid()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      */
isLayoutValid()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      */
resetContentDescription()241     public void resetContentDescription() {
242         mContentDescription = null;
243     }
244 
245     /**
246      * Get conversation information to use for accessibility.
247      */
getContentDescription(Context context, boolean showToHeader, String foldersDesc)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 
onAccessibilityUpdated()311     public static void onAccessibilityUpdated() {
312         sConversationHeaderMap.evictAll();
313     }
314 
315     /**
316      * Clear cached header model objects when the folder changes.
317      */
onFolderUpdated(Folder folder)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 
getEmailAddress()336         public String getEmailAddress() {
337             return mEmailAddress;
338         }
339 
getName()340         public String getName() {
341             return mName;
342         }
343 
344         /**
345          * Removes the name and email address of the participant of this avatar.
346          */
clear()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          */
populate(String name, String emailAddress)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          */
isNotPopulated()369         public boolean isNotPopulated() {
370             return TextUtils.isEmpty(mEmailAddress);
371         }
372     }
373 }
374