ConversationItemViewModel.java revision 4f2224c70d4d07df85b325fa3faf78218f92aae6
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.collect.Lists; 22 23import android.content.Context; 24import android.database.Cursor; 25import android.graphics.Bitmap; 26import android.text.SpannableString; 27import android.text.SpannableStringBuilder; 28import android.text.StaticLayout; 29import android.text.TextUtils; 30import android.text.style.CharacterStyle; 31import android.util.LruCache; 32import android.util.Pair; 33 34import com.android.mail.R; 35import com.android.mail.providers.Conversation; 36import com.android.mail.providers.Folder; 37import com.android.mail.providers.UIProvider; 38 39import java.util.ArrayList; 40 41/** 42 * This is the view model for the conversation header. It includes all the 43 * information needed to layout a conversation header view. Each view model is 44 * associated with a conversation and is cached to improve the relayout time. 45 */ 46public class ConversationItemViewModel { 47 private static final int MAX_CACHE_SIZE = 100; 48 49 boolean faded = false; 50 int fontColor; 51 @VisibleForTesting 52 static LruCache<Pair<String, Long>, ConversationItemViewModel> sConversationHeaderMap 53 = new LruCache<Pair<String, Long>, ConversationItemViewModel>(MAX_CACHE_SIZE); 54 55 // The hashcode used to detect if the conversation has changed. 56 private int mDataHashCode; 57 private int mLayoutHashCode; 58 59 // Unread 60 boolean unread; 61 62 // Date 63 String dateText; 64 Bitmap dateBackground; 65 66 // Personal level 67 Bitmap personalLevelBitmap; 68 69 // Paperclip 70 Bitmap paperclip; 71 72 // Senders 73 String sendersText; 74 75 // A list of all the fragments that cover sendersText 76 final ArrayList<SenderFragment> senderFragments; 77 78 SpannableStringBuilder sendersDisplayText; 79 StaticLayout sendersDisplayLayout; 80 81 boolean hasDraftMessage; 82 83 // Subject 84 SpannableStringBuilder subjectText; 85 86 StaticLayout subjectLayout; 87 88 // View Width 89 public int viewWidth; 90 91 // Standard scaled dimen used to detect if the scale of text has changed. 92 public int standardScaledDimen; 93 94 public String fromSnippetInstructions; 95 96 public long maxMessageId; 97 98 public boolean checkboxVisible; 99 100 public Conversation conversation; 101 102 public ConversationItemView.ConversationItemFolderDisplayer folderDisplayer; 103 104 public ArrayList<Folder> rawFolders; 105 106 public int personalLevel; 107 108 public int priority; 109 110 public boolean hasBeenForwarded; 111 112 public boolean hasBeenRepliedTo; 113 114 public boolean isInvite; 115 116 public StaticLayout subjectLayoutActivated; 117 118 public SpannableStringBuilder subjectTextActivated; 119 120 public SpannableString[] styledSenders; 121 122 public SpannableStringBuilder styledSendersString; 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.rawFolders = conv.getRawFolders(); 154 header.personalLevel = conv.personalLevel; 155 header.priority = conv.priority; 156 header.hasBeenForwarded = 157 (conv.convFlags & UIProvider.ConversationFlags.FORWARDED) 158 == UIProvider.ConversationFlags.FORWARDED; 159 header.hasBeenRepliedTo = 160 (conv.convFlags & UIProvider.ConversationFlags.REPLIED) 161 == UIProvider.ConversationFlags.REPLIED; 162 header.isInvite = 163 (conv.convFlags & UIProvider.ConversationFlags.CALENDAR_INVITE) 164 == UIProvider.ConversationFlags.CALENDAR_INVITE; 165 } 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 * Clears all the current sender fragments. 211 */ 212 void clearSenderFragments() { 213 senderFragments.clear(); 214 } 215 216 /** 217 * Returns the hashcode to compare if the data in the header is valid. 218 */ 219 private static int getHashCode(Context context, String dateText, Object convInfo, 220 String rawFolders, boolean starred) { 221 if (dateText == null) { 222 return -1; 223 } 224 if (TextUtils.isEmpty(rawFolders)) { 225 rawFolders = ""; 226 } 227 return convInfo.hashCode() ^ dateText.hashCode() ^ rawFolders.hashCode() 228 ^ (starred ? 1 : 0); 229 } 230 231 /** 232 * Returns the layout hashcode to compare to see if the layout state has changed. 233 */ 234 private int getLayoutHashCode() { 235 return mDataHashCode ^ viewWidth ^ standardScaledDimen 236 ^ Boolean.valueOf(checkboxVisible).hashCode(); 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); 251 mLayoutHashCode = getLayoutHashCode(); 252 } 253 254 /** 255 * Returns if the data in this model is valid. 256 */ 257 boolean isDataValid(Context context) { 258 return mDataHashCode == getHashCode(context, dateText, getConvInfo(), 259 conversation.getRawFoldersString(), conversation.starred); 260 } 261 262 /** 263 * Returns if the layout in this model is valid. 264 */ 265 boolean isLayoutValid(Context context) { 266 return isDataValid(context) && mLayoutHashCode == getLayoutHashCode(); 267 } 268 269 /** 270 * Describes the style of a Senders fragment. 271 */ 272 static class SenderFragment { 273 // Indices that determine which substring of mSendersText we are 274 // displaying. 275 int start; 276 int end; 277 278 // The style to apply to the TextPaint object. 279 CharacterStyle style; 280 281 // Width of the fragment. 282 int width; 283 284 // Ellipsized text. 285 String ellipsizedText; 286 287 // Whether the fragment is fixed or not. 288 boolean isFixed; 289 290 // Should the fragment be displayed or not. 291 boolean shouldDisplay; 292 293 SenderFragment(int start, int end, CharSequence sendersText, CharacterStyle style, 294 boolean isFixed) { 295 this.start = start; 296 this.end = end; 297 this.style = style; 298 this.isFixed = isFixed; 299 } 300 } 301 302 /** 303 * Get conversation information to use for accessibility. 304 */ 305 public CharSequence getContentDescription(Context context) { 306 return context.getString(R.string.content_description, conversation.subject, 307 conversation.getSnippet()); 308 } 309}