ConversationItemViewModel.java revision 0b686764015284889d98b8e9f1abea8b27ce26bd
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.SpannableStringBuilder; 27import android.text.StaticLayout; 28import android.text.TextUtils; 29import android.text.style.CharacterStyle; 30import android.util.LruCache; 31import android.util.Pair; 32 33import com.android.mail.R; 34import com.android.mail.providers.Conversation; 35import com.android.mail.providers.UIProvider; 36import com.android.mail.providers.UIProvider.ConversationPersonalLevel; 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 /** 124 * Returns the view model for a conversation. If the model doesn't exist for this conversation 125 * null is returned. Note: this should only be called from the UI thread. 126 * 127 * @param account the account contains this conversation 128 * @param conversationId the Id of this conversation 129 * @return the view model for this conversation, or null 130 */ 131 @VisibleForTesting 132 static ConversationItemViewModel forConversationIdOrNull( 133 String account, long conversationId) { 134 final Pair<String, Long> key = new Pair<String, Long>(account, conversationId); 135 synchronized(sConversationHeaderMap) { 136 return sConversationHeaderMap.get(key); 137 } 138 } 139 140 static ConversationItemViewModel forCursor(Cursor cursor) { 141 return forConversation(new Conversation(cursor)); 142 } 143 144 145 static ConversationItemViewModel forConversation(Conversation conv) { 146 ConversationItemViewModel header = new ConversationItemViewModel(); 147 if (conv != null) { 148 header.faded = false; 149 header.checkboxVisible = true; 150 header.conversation = conv; 151 header.starred = conv.starred; 152 header.unread = !conv.read; 153 header.rawFolders = conv.rawFolders; 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, String fromSnippetInstructions) { 220 if (dateText == null) { 221 return -1; 222 } 223 if (TextUtils.isEmpty(fromSnippetInstructions)) { 224 fromSnippetInstructions = "fromSnippetInstructions"; 225 } 226 return fromSnippetInstructions.hashCode() ^ dateText.hashCode(); 227 } 228 229 /** 230 * Returns the layout hashcode to compare to see if thet layout state has changed. 231 */ 232 private int getLayoutHashCode() { 233 return mDataHashCode ^ viewWidth ^ standardScaledDimen ^ 234 Boolean.valueOf(checkboxVisible).hashCode(); 235 } 236 237 /** 238 * Marks this header as having valid data and layout. 239 */ 240 void validate(Context context) { 241 mDataHashCode = getHashCode(context, dateText, fromSnippetInstructions); 242 mLayoutHashCode = getLayoutHashCode(); 243 } 244 245 /** 246 * Returns if the data in this model is valid. 247 */ 248 boolean isDataValid(Context context) { 249 return mDataHashCode == getHashCode(context, dateText, fromSnippetInstructions); 250 } 251 252 /** 253 * Returns if the layout in this model is valid. 254 */ 255 boolean isLayoutValid(Context context) { 256 return isDataValid(context) && mLayoutHashCode == getLayoutHashCode(); 257 } 258 259 /** 260 * Describes the style of a Senders fragment. 261 */ 262 static class SenderFragment { 263 // Indices that determine which substring of mSendersText we are 264 // displaying. 265 int start; 266 int end; 267 268 // The style to apply to the TextPaint object. 269 CharacterStyle style; 270 271 // Width of the fragment. 272 int width; 273 274 // Ellipsized text. 275 String ellipsizedText; 276 277 // Whether the fragment is fixed or not. 278 boolean isFixed; 279 280 // Should the fragment be displayed or not. 281 boolean shouldDisplay; 282 283 SenderFragment(int start, int end, CharSequence sendersText, CharacterStyle style, 284 boolean isFixed) { 285 this.start = start; 286 this.end = end; 287 this.style = style; 288 this.isFixed = isFixed; 289 } 290 } 291 292 /** 293 * Get conversation information to use for accessibility. 294 */ 295 public CharSequence getContentDescription(Context context) { 296 return context.getString(R.string.content_description, conversation.subject, 297 conversation.snippet); 298 } 299}