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