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