ConversationItemViewModel.java revision 12fe37aa24c313fd8192dede0770dadf0ec23359
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.style.CharacterStyle; 33import android.util.LruCache; 34import android.util.Pair; 35 36import com.android.mail.R; 37import com.android.mail.providers.Conversation; 38import com.android.mail.providers.Folder; 39import com.android.mail.providers.UIProvider; 40 41import java.util.ArrayList; 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 */ 48public class ConversationItemViewModel { 49 private static final int MAX_CACHE_SIZE = 100; 50 51 boolean faded = false; 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 String dateText; 71 Bitmap dateBackground; 72 73 // Personal level 74 Bitmap personalLevelBitmap; 75 76 // Paperclip 77 Bitmap paperclip; 78 79 // Senders 80 String sendersText; 81 82 // A list of all the fragments that cover sendersText 83 final ArrayList<SenderFragment> senderFragments; 84 85 SpannableStringBuilder sendersDisplayText; 86 StaticLayout sendersDisplayLayout; 87 88 boolean hasDraftMessage; 89 90 // Subject 91 SpannableStringBuilder subjectText; 92 93 StaticLayout subjectLayout; 94 95 // View Width 96 public int viewWidth; 97 98 // Standard scaled dimen used to detect if the scale of text has changed. 99 public int standardScaledDimen; 100 101 public String fromSnippetInstructions; 102 103 public long maxMessageId; 104 105 public boolean checkboxVisible; 106 107 public Conversation conversation; 108 109 public ConversationItemView.ConversationItemFolderDisplayer folderDisplayer; 110 111 public boolean hasBeenForwarded; 112 113 public boolean hasBeenRepliedTo; 114 115 public boolean isInvite; 116 117 public StaticLayout subjectLayoutActivated; 118 119 public SpannableStringBuilder subjectTextActivated; 120 121 public SpannableString[] styledSenders; 122 123 public SpannableStringBuilder styledSendersString; 124 125 public SpannableStringBuilder messageInfoString; 126 127 public int styledMessageInfoStringOffset; 128 129 /** 130 * Returns the view model for a conversation. If the model doesn't exist for this conversation 131 * null is returned. Note: this should only be called from the UI thread. 132 * 133 * @param account the account contains this conversation 134 * @param conversationId the Id of this conversation 135 * @return the view model for this conversation, or null 136 */ 137 @VisibleForTesting 138 static ConversationItemViewModel forConversationIdOrNull( 139 String account, long conversationId) { 140 final Pair<String, Long> key = new Pair<String, Long>(account, conversationId); 141 synchronized(sConversationHeaderMap) { 142 return sConversationHeaderMap.get(key); 143 } 144 } 145 146 static ConversationItemViewModel forCursor(String account, Cursor cursor) { 147 return forConversation(account, new Conversation(cursor)); 148 } 149 150 static ConversationItemViewModel forConversation(String account, Conversation conv) { 151 ConversationItemViewModel header = ConversationItemViewModel.forConversationId(account, 152 conv.id); 153 if (conv != null) { 154 header.faded = false; 155 header.checkboxVisible = true; 156 header.conversation = conv; 157 header.unread = !conv.read; 158 header.hasBeenForwarded = 159 (conv.convFlags & UIProvider.ConversationFlags.FORWARDED) 160 == UIProvider.ConversationFlags.FORWARDED; 161 header.hasBeenRepliedTo = 162 (conv.convFlags & UIProvider.ConversationFlags.REPLIED) 163 == UIProvider.ConversationFlags.REPLIED; 164 header.isInvite = 165 (conv.convFlags & UIProvider.ConversationFlags.CALENDAR_INVITE) 166 == UIProvider.ConversationFlags.CALENDAR_INVITE; 167 } 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 * @param cursor the cursor to use in populating/ updating the model. 179 * @return the view model for this conversation 180 */ 181 static ConversationItemViewModel forConversationId(String account, long conversationId) { 182 synchronized(sConversationHeaderMap) { 183 ConversationItemViewModel header = 184 forConversationIdOrNull(account, conversationId); 185 if (header == null) { 186 final Pair<String, Long> key = new Pair<String, Long>(account, conversationId); 187 header = new ConversationItemViewModel(); 188 sConversationHeaderMap.put(key, header); 189 } 190 return header; 191 } 192 } 193 194 public ConversationItemViewModel() { 195 senderFragments = Lists.newArrayList(); 196 } 197 198 /** 199 * Adds a sender fragment. 200 * 201 * @param start the start position of this fragment 202 * @param end the start position of this fragment 203 * @param style the style of this fragment 204 * @param isFixed whether this fragment is fixed or not 205 */ 206 void addSenderFragment(int start, int end, CharacterStyle style, boolean isFixed) { 207 SenderFragment senderFragment = new SenderFragment(start, end, sendersText, style, isFixed); 208 senderFragments.add(senderFragment); 209 } 210 211 /** 212 * Clears all the current sender fragments. 213 */ 214 void clearSenderFragments() { 215 senderFragments.clear(); 216 } 217 218 /** 219 * Returns the hashcode to compare if the data in the header is valid. 220 */ 221 private static int getHashCode(Context context, String dateText, Object convInfo, 222 String rawFolders, boolean starred, boolean read, int priority) { 223 if (dateText == null) { 224 return -1; 225 } 226 if (TextUtils.isEmpty(rawFolders)) { 227 rawFolders = ""; 228 } 229 return Objects.hashCode(convInfo, dateText, rawFolders, starred, read, priority); 230 } 231 232 /** 233 * Returns the layout hashcode to compare to see if the layout state has changed. 234 */ 235 private int getLayoutHashCode() { 236 return Objects.hashCode(mDataHashCode, viewWidth, standardScaledDimen, checkboxVisible); 237 } 238 239 private Object getConvInfo() { 240 return conversation.conversationInfo != null ? 241 conversation.conversationInfo : 242 TextUtils.isEmpty(fromSnippetInstructions) ? "" : fromSnippetInstructions; 243 } 244 245 /** 246 * Marks this header as having valid data and layout. 247 */ 248 void validate(Context context) { 249 mDataHashCode = getHashCode(context, dateText, getConvInfo(), 250 conversation.getRawFoldersString(), conversation.starred, conversation.read, 251 conversation.priority); 252 mLayoutHashCode = getLayoutHashCode(); 253 } 254 255 /** 256 * Returns if the data in this model is valid. 257 */ 258 boolean isDataValid(Context context) { 259 return mDataHashCode == getHashCode(context, dateText, getConvInfo(), 260 conversation.getRawFoldersString(), conversation.starred, conversation.read, 261 conversation.priority); 262 } 263 264 /** 265 * Returns if the layout in this model is valid. 266 */ 267 boolean isLayoutValid(Context context) { 268 return isDataValid(context) && mLayoutHashCode == getLayoutHashCode(); 269 } 270 271 /** 272 * Describes the style of a Senders fragment. 273 */ 274 static class SenderFragment { 275 // Indices that determine which substring of mSendersText we are 276 // displaying. 277 int start; 278 int end; 279 280 // The style to apply to the TextPaint object. 281 CharacterStyle style; 282 283 // Width of the fragment. 284 int width; 285 286 // Ellipsized text. 287 String ellipsizedText; 288 289 // Whether the fragment is fixed or not. 290 boolean isFixed; 291 292 // Should the fragment be displayed or not. 293 boolean shouldDisplay; 294 295 SenderFragment(int start, int end, CharSequence sendersText, CharacterStyle style, 296 boolean isFixed) { 297 this.start = start; 298 this.end = end; 299 this.style = style; 300 this.isFixed = isFixed; 301 } 302 } 303 304 /** 305 * Get conversation information to use for accessibility. 306 */ 307 public CharSequence getContentDescription(Context context) { 308 return context.getString(R.string.content_description, conversation.subject, 309 conversation.getSnippet()); 310 } 311 312 /** 313 * Clear cached header model objects when the folder changes. 314 */ 315 public static void onFolderUpdated(Folder folder) { 316 Uri old = sCachedModelsFolder != null ? sCachedModelsFolder.uri : Uri.EMPTY; 317 Uri newUri = folder != null ? folder.uri : Uri.EMPTY; 318 if (!old.equals(newUri)) { 319 sCachedModelsFolder = folder; 320 sConversationHeaderMap.evictAll(); 321 } 322 } 323}