SendersView.java revision d25c753fd348cbd98a148e434134bbe2191aad7d
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 android.content.BroadcastReceiver; 21import android.content.Context; 22import android.content.Intent; 23import android.content.IntentFilter; 24import android.content.res.Resources; 25import android.graphics.Typeface; 26import android.text.Spannable; 27import android.text.SpannableString; 28import android.text.SpannableStringBuilder; 29import android.text.TextUtils; 30import android.text.style.CharacterStyle; 31import android.text.style.TextAppearanceSpan; 32import android.text.util.Rfc822Token; 33import android.text.util.Rfc822Tokenizer; 34 35import com.android.mail.R; 36import com.android.mail.providers.Address; 37import com.android.mail.providers.Conversation; 38import com.android.mail.providers.ConversationInfo; 39import com.android.mail.providers.MessageInfo; 40import com.android.mail.providers.UIProvider; 41import com.android.mail.ui.DividedImageCanvas; 42import com.android.mail.utils.ObjectCache; 43import com.google.common.base.Objects; 44import com.google.common.collect.Maps; 45 46import java.util.ArrayList; 47import java.util.Locale; 48import java.util.Map; 49 50import java.util.regex.Pattern; 51 52public class SendersView { 53 public static final int DEFAULT_FORMATTING = 0; 54 public static final int MERGED_FORMATTING = 1; 55 private static final Integer DOES_NOT_EXIST = -5; 56 private static String sSendersSplitToken; 57 public static String SENDERS_VERSION_SEPARATOR = "^**^"; 58 public static Pattern SENDERS_VERSION_SEPARATOR_PATTERN = Pattern.compile("\\^\\*\\*\\^"); 59 private static CharSequence sDraftSingularString; 60 private static CharSequence sDraftPluralString; 61 private static CharSequence sSendingString; 62 private static String sDraftCountFormatString; 63 private static CharacterStyle sDraftsStyleSpan; 64 private static CharacterStyle sSendingStyleSpan; 65 private static TextAppearanceSpan sUnreadStyleSpan; 66 private static CharacterStyle sReadStyleSpan; 67 private static String sMeString; 68 private static Locale sMeStringLocale; 69 private static String sMessageCountSpacerString; 70 public static CharSequence sElidedString; 71 private static BroadcastReceiver sConfigurationChangedReceiver; 72 private static TextAppearanceSpan sMessageInfoReadStyleSpan; 73 private static TextAppearanceSpan sMessageInfoUnreadStyleSpan; 74 75 // We only want to have at most 2 Priority to length maps. This will handle the case where 76 // there is a widget installed on the launcher while the user is scrolling in the app 77 private static final int MAX_PRIORITY_LENGTH_MAP_LIST = 2; 78 79 // Cache of priority to length maps. We can't just use a single instance as it may be 80 // modified from different threads 81 private static final ObjectCache<Map<Integer, Integer>> PRIORITY_LENGTH_MAP_CACHE = 82 new ObjectCache<Map<Integer, Integer>>( 83 new ObjectCache.Callback<Map<Integer, Integer>>() { 84 @Override 85 public Map<Integer, Integer> newInstance() { 86 return Maps.newHashMap(); 87 } 88 @Override 89 public void onObjectReleased(Map<Integer, Integer> object) { 90 object.clear(); 91 } 92 }, MAX_PRIORITY_LENGTH_MAP_LIST); 93 94 public static Typeface getTypeface(boolean isUnread) { 95 return isUnread ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT; 96 } 97 98 private static void getSenderResources(Context context) { 99 if (sConfigurationChangedReceiver == null) { 100 sConfigurationChangedReceiver = new BroadcastReceiver() { 101 @Override 102 public void onReceive(Context context, Intent intent) { 103 sDraftSingularString = null; 104 getSenderResources(context); 105 } 106 }; 107 context.registerReceiver(sConfigurationChangedReceiver, new IntentFilter( 108 Intent.ACTION_CONFIGURATION_CHANGED)); 109 } 110 if (sDraftSingularString == null) { 111 Resources res = context.getResources(); 112 sSendersSplitToken = res.getString(R.string.senders_split_token); 113 sElidedString = res.getString(R.string.senders_elided); 114 sDraftSingularString = res.getQuantityText(R.plurals.draft, 1); 115 sDraftPluralString = res.getQuantityText(R.plurals.draft, 2); 116 sDraftCountFormatString = res.getString(R.string.draft_count_format); 117 sMessageInfoUnreadStyleSpan = new TextAppearanceSpan(context, 118 R.style.MessageInfoUnreadTextAppearance); 119 sMessageInfoReadStyleSpan = new TextAppearanceSpan(context, 120 R.style.MessageInfoReadTextAppearance); 121 sDraftsStyleSpan = new TextAppearanceSpan(context, R.style.DraftTextAppearance); 122 sUnreadStyleSpan = new TextAppearanceSpan(context, R.style.SendersUnreadTextAppearance); 123 sSendingStyleSpan = new TextAppearanceSpan(context, R.style.SendingTextAppearance); 124 sReadStyleSpan = new TextAppearanceSpan(context, R.style.SendersReadTextAppearance); 125 sMessageCountSpacerString = res.getString(R.string.message_count_spacer); 126 sSendingString = res.getString(R.string.sending); 127 } 128 } 129 130 public static SpannableStringBuilder createMessageInfo(Context context, Conversation conv) { 131 ConversationInfo conversationInfo = conv.conversationInfo; 132 int sendingStatus = conv.sendingState; 133 SpannableStringBuilder messageInfo = new SpannableStringBuilder(); 134 boolean hasSenders = false; 135 // This covers the case where the sender is "me" and this is a draft 136 // message, which means this will only run once most of the time. 137 for (MessageInfo m : conversationInfo.messageInfos) { 138 if (!TextUtils.isEmpty(m.sender)) { 139 hasSenders = true; 140 break; 141 } 142 } 143 getSenderResources(context); 144 if (conversationInfo != null) { 145 int count = conversationInfo.messageCount; 146 int draftCount = conversationInfo.draftCount; 147 boolean showSending = sendingStatus == UIProvider.ConversationSendingState.SENDING; 148 if (count > 1) { 149 messageInfo.append(count + ""); 150 } 151 messageInfo.setSpan(CharacterStyle.wrap( 152 conv.read ? sMessageInfoReadStyleSpan : sMessageInfoUnreadStyleSpan), 153 0, messageInfo.length(), 0); 154 if (draftCount > 0) { 155 // If we are showing a message count or any draft text and there 156 // is at least 1 sender, prepend the sending state text with a 157 // comma. 158 if (hasSenders || count > 1) { 159 messageInfo.append(sSendersSplitToken); 160 } 161 SpannableStringBuilder draftString = new SpannableStringBuilder(); 162 if (draftCount == 1) { 163 draftString.append(sDraftSingularString); 164 } else { 165 draftString.append(sDraftPluralString 166 + String.format(sDraftCountFormatString, draftCount)); 167 } 168 draftString.setSpan(CharacterStyle.wrap(sDraftsStyleSpan), 0, draftString.length(), 169 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 170 messageInfo.append(draftString); 171 } 172 if (showSending) { 173 // If we are showing a message count or any draft text, prepend 174 // the sending state text with a comma. 175 if (count > 1 || draftCount > 0) { 176 messageInfo.append(sSendersSplitToken); 177 } 178 SpannableStringBuilder sending = new SpannableStringBuilder(); 179 sending.append(sSendingString); 180 sending.setSpan(sSendingStyleSpan, 0, sending.length(), 0); 181 messageInfo.append(sending); 182 } 183 // Prepend a space if we are showing other message info text. 184 if (count > 1 || (draftCount > 0 && hasSenders) || showSending) { 185 messageInfo = new SpannableStringBuilder(sMessageCountSpacerString) 186 .append(messageInfo); 187 } 188 } 189 return messageInfo; 190 } 191 192 public static void format(Context context, ConversationInfo conversationInfo, 193 String messageInfo, int maxChars, ArrayList<SpannableString> styledSenders, 194 ArrayList<String> displayableSenderNames, ArrayList<String> displayableSenderEmails, 195 String account) { 196 getSenderResources(context); 197 format(context, conversationInfo, messageInfo, maxChars, styledSenders, 198 displayableSenderNames, displayableSenderEmails, account, 199 sUnreadStyleSpan, sReadStyleSpan); 200 } 201 202 public static void format(Context context, ConversationInfo conversationInfo, 203 String messageInfo, int maxChars, ArrayList<SpannableString> styledSenders, 204 ArrayList<String> displayableSenderNames, ArrayList<String> displayableSenderEmails, 205 String account, final TextAppearanceSpan notificationUnreadStyleSpan, 206 final CharacterStyle notificationReadStyleSpan) { 207 getSenderResources(context); 208 handlePriority(context, maxChars, messageInfo, conversationInfo, styledSenders, 209 displayableSenderNames, displayableSenderEmails, account, 210 notificationUnreadStyleSpan, notificationReadStyleSpan); 211 } 212 213 public static void handlePriority(Context context, int maxChars, String messageInfoString, 214 ConversationInfo conversationInfo, ArrayList<SpannableString> styledSenders, 215 ArrayList<String> displayableSenderNames, ArrayList<String> displayableSenderEmails, 216 String account, final TextAppearanceSpan unreadStyleSpan, 217 final CharacterStyle readStyleSpan) { 218 boolean shouldAddPhotos = displayableSenderEmails != null; 219 int maxPriorityToInclude = -1; // inclusive 220 int numCharsUsed = messageInfoString.length(); // draft, number drafts, 221 // count 222 int numSendersUsed = 0; 223 int numCharsToRemovePerWord = 0; 224 int maxFoundPriority = 0; 225 if (numCharsUsed > maxChars) { 226 numCharsToRemovePerWord = numCharsUsed - maxChars; 227 } 228 229 final Map<Integer, Integer> priorityToLength = PRIORITY_LENGTH_MAP_CACHE.get(); 230 try { 231 priorityToLength.clear(); 232 int senderLength; 233 for (MessageInfo info : conversationInfo.messageInfos) { 234 senderLength = !TextUtils.isEmpty(info.sender) ? info.sender.length() : 0; 235 priorityToLength.put(info.priority, senderLength); 236 maxFoundPriority = Math.max(maxFoundPriority, info.priority); 237 } 238 while (maxPriorityToInclude < maxFoundPriority) { 239 if (priorityToLength.containsKey(maxPriorityToInclude + 1)) { 240 int length = numCharsUsed + priorityToLength.get(maxPriorityToInclude + 1); 241 if (numCharsUsed > 0) 242 length += 2; 243 // We must show at least two senders if they exist. If we don't 244 // have space for both 245 // then we will truncate names. 246 if (length > maxChars && numSendersUsed >= 2) { 247 break; 248 } 249 numCharsUsed = length; 250 numSendersUsed++; 251 } 252 maxPriorityToInclude++; 253 } 254 } finally { 255 PRIORITY_LENGTH_MAP_CACHE.release(priorityToLength); 256 } 257 // We want to include this entry if 258 // 1) The onlyShowUnread flags is not set 259 // 2) The above flag is set, and the message is unread 260 MessageInfo currentMessage; 261 SpannableString spannableDisplay; 262 String nameString; 263 CharacterStyle style; 264 boolean appendedElided = false; 265 Map<String, Integer> displayHash = Maps.newHashMap(); 266 String firstDisplayableSenderEmail = null; 267 String firstDisplayableSender = null; 268 for (int i = 0; i < conversationInfo.messageInfos.size(); i++) { 269 currentMessage = conversationInfo.messageInfos.get(i); 270 nameString = !TextUtils.isEmpty(currentMessage.sender) ? currentMessage.sender : ""; 271 if (nameString.length() == 0) { 272 nameString = getMe(context); 273 } 274 if (numCharsToRemovePerWord != 0) { 275 nameString = nameString.substring(0, 276 Math.max(nameString.length() - numCharsToRemovePerWord, 0)); 277 } 278 final int priority = currentMessage.priority; 279 style = !currentMessage.read ? getWrappedStyleSpan(unreadStyleSpan) 280 : getWrappedStyleSpan(readStyleSpan); 281 if (priority <= maxPriorityToInclude) { 282 spannableDisplay = new SpannableString(nameString); 283 // Don't duplicate senders; leave the first instance, unless the 284 // current instance is also unread. 285 int oldPos = displayHash.containsKey(currentMessage.sender) ? displayHash 286 .get(currentMessage.sender) : DOES_NOT_EXIST; 287 // If this sender doesn't exist OR the current message is 288 // unread, add the sender. 289 if (oldPos == DOES_NOT_EXIST || !currentMessage.read) { 290 // If the sender entry already existed, and is right next to the 291 // current sender, remove the old entry. 292 if (oldPos != DOES_NOT_EXIST && i > 0 && oldPos == i - 1 293 && oldPos < styledSenders.size()) { 294 // Remove the old one! 295 styledSenders.set(oldPos, null); 296 if (shouldAddPhotos && !TextUtils.isEmpty(currentMessage.senderEmail)) { 297 displayableSenderEmails.remove(currentMessage.senderEmail); 298 displayableSenderNames.remove(currentMessage.sender); 299 } 300 } 301 displayHash.put(currentMessage.sender, i); 302 spannableDisplay.setSpan(style, 0, spannableDisplay.length(), 0); 303 styledSenders.add(spannableDisplay); 304 } 305 } else { 306 if (!appendedElided) { 307 spannableDisplay = new SpannableString(sElidedString); 308 spannableDisplay.setSpan(style, 0, spannableDisplay.length(), 0); 309 appendedElided = true; 310 styledSenders.add(spannableDisplay); 311 } 312 } 313 if (shouldAddPhotos) { 314 String senderEmail = TextUtils.isEmpty(currentMessage.sender) ? 315 account : 316 TextUtils.isEmpty(currentMessage.senderEmail) ? 317 currentMessage.sender : currentMessage.senderEmail; 318 if (i == 0) { 319 // Always add the first sender! 320 firstDisplayableSenderEmail = senderEmail; 321 firstDisplayableSender = currentMessage.sender; 322 } else { 323 if (!Objects.equal(firstDisplayableSenderEmail, senderEmail)) { 324 int indexOf = displayableSenderEmails.indexOf(senderEmail); 325 if (indexOf > -1) { 326 displayableSenderEmails.remove(indexOf); 327 displayableSenderNames.remove(indexOf); 328 } 329 displayableSenderEmails.add(senderEmail); 330 displayableSenderNames.add(currentMessage.sender); 331 if (displayableSenderEmails.size() > DividedImageCanvas.MAX_DIVISIONS) { 332 displayableSenderEmails.remove(0); 333 displayableSenderNames.remove(0); 334 } 335 } 336 } 337 } 338 } 339 if (shouldAddPhotos && !TextUtils.isEmpty(firstDisplayableSenderEmail)) { 340 if (displayableSenderEmails.size() < DividedImageCanvas.MAX_DIVISIONS) { 341 displayableSenderEmails.add(0, firstDisplayableSenderEmail); 342 displayableSenderNames.add(0, firstDisplayableSender); 343 } else { 344 displayableSenderEmails.set(0, firstDisplayableSenderEmail); 345 displayableSenderNames.set(0, firstDisplayableSender); 346 } 347 } 348 } 349 350 private static CharacterStyle getWrappedStyleSpan(final CharacterStyle characterStyle) { 351 return CharacterStyle.wrap(characterStyle); 352 } 353 354 static String getMe(Context context) { 355 final Resources resources = context.getResources(); 356 final Locale locale = resources.getConfiguration().locale; 357 358 if (sMeString == null || !locale.equals(sMeStringLocale)) { 359 sMeString = resources.getString(R.string.me); 360 sMeStringLocale = locale; 361 } 362 return sMeString; 363 } 364 365 private static void formatDefault(ConversationItemViewModel header, String sendersString, 366 Context context, final CharacterStyle readStyleSpan) { 367 getSenderResources(context); 368 // Clear any existing sender fragments; we must re-make all of them. 369 header.senderFragments.clear(); 370 String[] senders = TextUtils.split(sendersString, Address.ADDRESS_DELIMETER); 371 String[] namesOnly = new String[senders.length]; 372 Rfc822Token[] senderTokens; 373 String display; 374 for (int i = 0; i < senders.length; i++) { 375 senderTokens = Rfc822Tokenizer.tokenize(senders[i]); 376 if (senderTokens != null && senderTokens.length > 0) { 377 display = senderTokens[0].getName(); 378 if (TextUtils.isEmpty(display)) { 379 display = senderTokens[0].getAddress(); 380 } 381 namesOnly[i] = display; 382 } 383 } 384 generateSenderFragments(header, namesOnly, readStyleSpan); 385 } 386 387 private static void generateSenderFragments(ConversationItemViewModel header, String[] names, 388 final CharacterStyle readStyleSpan) { 389 header.sendersText = TextUtils.join(Address.ADDRESS_DELIMETER + " ", names); 390 header.addSenderFragment(0, header.sendersText.length(), getWrappedStyleSpan(readStyleSpan), 391 true); 392 } 393 394 public static void formatSenders(ConversationItemViewModel header, Context context) { 395 getSenderResources(context); 396 formatSenders(header, context, sReadStyleSpan); 397 } 398 399 public static void formatSenders(ConversationItemViewModel header, Context context, 400 final CharacterStyle readStyleSpan) { 401 formatDefault(header, header.conversation.senders, context, readStyleSpan); 402 } 403} 404