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