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