SendersView.java revision 77d7f3c3c8c82048b51025428a825f6ac71e8560
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.Configuration; 25import android.content.res.Resources; 26import android.graphics.Typeface; 27import android.text.Html; 28import android.text.Spannable; 29import android.text.SpannableString; 30import android.text.SpannableStringBuilder; 31import android.text.TextUtils; 32import android.text.style.CharacterStyle; 33import android.text.style.TextAppearanceSpan; 34import android.text.util.Rfc822Token; 35import android.text.util.Rfc822Tokenizer; 36import android.util.AttributeSet; 37import android.widget.TextView; 38 39import com.android.mail.R; 40import com.android.mail.providers.Address; 41import com.android.mail.providers.Conversation; 42import com.android.mail.providers.ConversationInfo; 43import com.android.mail.providers.MessageInfo; 44import com.android.mail.providers.UIProvider; 45import com.google.common.annotations.VisibleForTesting; 46import com.google.common.collect.Maps; 47 48import java.util.ArrayList; 49import java.util.Map; 50 51import java.util.regex.Pattern; 52 53public class SendersView { 54 public static final int DEFAULT_FORMATTING = 0; 55 public static final int MERGED_FORMATTING = 1; 56 private static final Integer DOES_NOT_EXIST = -5; 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 sMessageInfoStyleSpan; 65 private static CharacterStyle sDraftsStyleSpan; 66 private static CharacterStyle sSendingStyleSpan; 67 private static CharacterStyle sUnreadStyleSpan; 68 private static CharacterStyle sReadStyleSpan; 69 private static String sMeString; 70 private static String sMessageCountSpacerString; 71 public static CharSequence sElidedString; 72 private static Map<Integer, Integer> sPriorityToLength; 73 private static BroadcastReceiver sConfigurationChangedReceiver; 74 75 public static Typeface getTypeface(boolean isUnread) { 76 return isUnread ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT; 77 } 78 79 private static void getSenderResources(Context context) { 80 if (sConfigurationChangedReceiver == null) { 81 sConfigurationChangedReceiver = new BroadcastReceiver() { 82 @Override 83 public void onReceive(Context context, Intent intent) { 84 sDraftSingularString = null; 85 getSenderResources(context); 86 } 87 }; 88 context.registerReceiver(sConfigurationChangedReceiver, new IntentFilter( 89 Intent.ACTION_CONFIGURATION_CHANGED)); 90 } 91 if (sDraftSingularString == null) { 92 Resources res = context.getResources(); 93 sSendersSplitToken = res.getString(R.string.senders_split_token); 94 sElidedString = res.getString(R.string.senders_elided); 95 sDraftSingularString = res.getQuantityText(R.plurals.draft, 1); 96 sDraftPluralString = res.getQuantityText(R.plurals.draft, 2); 97 sDraftCountFormatString = res.getString(R.string.draft_count_format); 98 sMessageInfoStyleSpan = new TextAppearanceSpan(context, 99 R.style.MessageInfoTextAppearance); 100 sDraftsStyleSpan = new TextAppearanceSpan(context, R.style.DraftTextAppearance); 101 sUnreadStyleSpan = new TextAppearanceSpan(context, 102 R.style.SendersUnreadTextAppearance); 103 sSendingStyleSpan = new TextAppearanceSpan(context, R.style.SendingTextAppearance); 104 sReadStyleSpan = new TextAppearanceSpan(context, R.style.SendersReadTextAppearance); 105 sMessageCountSpacerString = res.getString(R.string.message_count_spacer); 106 sSendingString = res.getString(R.string.sending); 107 } 108 } 109 110 public static SpannableStringBuilder createMessageInfo(Context context, 111 Conversation conv) { 112 ConversationInfo conversationInfo = conv.conversationInfo; 113 int sendingStatus = conv.sendingState; 114 SpannableStringBuilder messageInfo = new SpannableStringBuilder(); 115 getSenderResources(context); 116 if (conversationInfo != null) { 117 int count = conversationInfo.messageCount; 118 int draftCount = conversationInfo.draftCount; 119 if (count > 0 || draftCount <= 0) { 120 messageInfo.append(sMessageCountSpacerString); 121 } 122 if (count > 1) { 123 messageInfo.append(count + ""); 124 } 125 messageInfo.setSpan(CharacterStyle.wrap(sMessageInfoStyleSpan), 0, 126 messageInfo.length(), 0); 127 if (draftCount > 0) { 128 messageInfo.append(sSendersSplitToken); 129 SpannableStringBuilder draftString = new SpannableStringBuilder(); 130 if (draftCount == 1) { 131 draftString.append(sDraftSingularString); 132 } else { 133 draftString.append(sDraftPluralString 134 + String.format(sDraftCountFormatString, draftCount)); 135 } 136 draftString.setSpan(CharacterStyle.wrap(sDraftsStyleSpan), 0, draftString.length(), 137 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 138 messageInfo.append(draftString); 139 } 140 if (sendingStatus == UIProvider.ConversationSendingState.SENDING) { 141 if (count > 0 ||draftCount > 0) { 142 messageInfo.append(sSendersSplitToken); 143 } 144 SpannableStringBuilder sending = new SpannableStringBuilder(); 145 sending.append(sSendingString); 146 sending.setSpan(sSendingStyleSpan, 0, sending.length(), 0); 147 messageInfo.append(sending); 148 } 149 } 150 return messageInfo; 151 } 152 153 @VisibleForTesting 154 public static SpannableString[] format(Context context, 155 ConversationInfo conversationInfo, String messageInfo, int maxChars) { 156 getSenderResources(context); 157 ArrayList<SpannableString> displays = handlePriority(context, maxChars, 158 messageInfo.toString(), conversationInfo); 159 return displays.toArray(new SpannableString[displays.size()]); 160 } 161 162 public static ArrayList<SpannableString> handlePriority(Context context, int maxChars, 163 String messageInfoString, ConversationInfo conversationInfo) { 164 int maxPriorityToInclude = -1; // inclusive 165 int numCharsUsed = messageInfoString.length(); // draft, number drafts, 166 // count 167 int numSendersUsed = 0; 168 int numCharsToRemovePerWord = 0; 169 int maxFoundPriority = 0; 170 if (numCharsUsed > maxChars) { 171 numCharsToRemovePerWord = (numCharsUsed - maxChars) / numSendersUsed; 172 } 173 if (sPriorityToLength == null) { 174 sPriorityToLength = Maps.newHashMap(); 175 } 176 final Map<Integer, Integer> priorityToLength = sPriorityToLength; 177 priorityToLength.clear(); 178 int senderLength; 179 for (MessageInfo info : conversationInfo.messageInfos) { 180 senderLength = !TextUtils.isEmpty(info.sender) ? info.sender.length() : 0; 181 priorityToLength.put(info.priority, senderLength); 182 maxFoundPriority = Math.max(maxFoundPriority, info.priority); 183 } 184 while (maxPriorityToInclude < maxFoundPriority) { 185 if (priorityToLength.containsKey(maxPriorityToInclude + 1)) { 186 int length = numCharsUsed + priorityToLength.get(maxPriorityToInclude + 1); 187 if (numCharsUsed > 0) 188 length += 2; 189 // We must show at least two senders if they exist. If we don't 190 // have space for both 191 // then we will truncate names. 192 if (length > maxChars && numSendersUsed >= 2) { 193 break; 194 } 195 numCharsUsed = length; 196 numSendersUsed++; 197 } 198 maxPriorityToInclude++; 199 } 200 // We want to include this entry if 201 // 1) The onlyShowUnread flags is not set 202 // 2) The above flag is set, and the message is unread 203 MessageInfo currentMessage; 204 ArrayList<SpannableString> senders = new ArrayList<SpannableString>(); 205 SpannableString spannableDisplay; 206 String nameString; 207 CharacterStyle style; 208 boolean appendedElided = false; 209 Map<String, Integer> displayHash = Maps.newHashMap(); 210 for (int i = 0; i < conversationInfo.messageInfos.size(); i++) { 211 currentMessage = conversationInfo.messageInfos.get(i); 212 nameString = !TextUtils.isEmpty(currentMessage.sender) ? currentMessage.sender : ""; 213 if (nameString.length() == 0) { 214 nameString = getMe(context); 215 } else { 216 nameString = Html.fromHtml(nameString).toString(); 217 } 218 if (numCharsToRemovePerWord != 0) { 219 nameString = nameString.substring(0, 220 Math.max(nameString.length() - numCharsToRemovePerWord, 0)); 221 } 222 final int priority = currentMessage.priority; 223 style = !currentMessage.read ? getUnreadStyleSpan() : getReadStyleSpan(); 224 if (priority <= maxPriorityToInclude) { 225 spannableDisplay = new SpannableString(nameString); 226 // Don't duplicate senders; leave the first instance, unless the 227 // current instance is also unread. 228 int oldPos = displayHash.containsKey(currentMessage.sender) ? displayHash 229 .get(currentMessage.sender) : DOES_NOT_EXIST; 230 // If this sender doesn't exist OR the current message is 231 // unread, add the sender. 232 if (oldPos == DOES_NOT_EXIST || !currentMessage.read) { 233 // If the sender entry already existed, and is right next to the 234 // current sender, remove the old entry. 235 if (oldPos != DOES_NOT_EXIST && i > 0 && oldPos == i - 1) { 236 // Remove the old one! 237 senders.remove(oldPos); 238 } 239 displayHash.put(currentMessage.sender, i); 240 spannableDisplay.setSpan(style, 0, spannableDisplay.length(), 0); 241 senders.add(spannableDisplay); 242 } 243 } else { 244 if (!appendedElided) { 245 spannableDisplay = new SpannableString(sElidedString); 246 spannableDisplay.setSpan(style, 0, spannableDisplay.length(), 0); 247 appendedElided = true; 248 senders.add(spannableDisplay); 249 } 250 } 251 } 252 return senders; 253 } 254 255 private static CharacterStyle getUnreadStyleSpan() { 256 return CharacterStyle.wrap(sUnreadStyleSpan); 257 } 258 259 private static CharacterStyle getReadStyleSpan() { 260 return CharacterStyle.wrap(sReadStyleSpan); 261 } 262 263 private static String getMe(Context context) { 264 if (sMeString == null) { 265 sMeString = context.getResources().getString(R.string.me); 266 } 267 return sMeString; 268 } 269 270 private static void formatDefault(ConversationItemViewModel header, String sendersString, 271 Context context) { 272 getSenderResources(context); 273 // Clear any existing sender fragments; we must re-make all of them. 274 header.senderFragments.clear(); 275 String[] senders = TextUtils.split(sendersString, Address.ADDRESS_DELIMETER); 276 String[] namesOnly = new String[senders.length]; 277 Rfc822Token[] senderTokens; 278 String display; 279 for (int i = 0; i < senders.length; i++) { 280 senderTokens = Rfc822Tokenizer.tokenize(senders[i]); 281 if (senderTokens != null && senderTokens.length > 0) { 282 display = senderTokens[0].getName(); 283 if (TextUtils.isEmpty(display)) { 284 display = senderTokens[0].getAddress(); 285 } 286 namesOnly[i] = display; 287 } 288 } 289 generateSenderFragments(header, namesOnly); 290 } 291 292 private static void generateSenderFragments(ConversationItemViewModel header, String[] names) { 293 header.sendersText = TextUtils.join(Address.ADDRESS_DELIMETER + " ", names); 294 header.addSenderFragment(0, header.sendersText.length(), getReadStyleSpan(), true); 295 } 296 297 public static void formatSenders(ConversationItemViewModel header, Context context) { 298 formatDefault(header, header.conversation.senders, context); 299 } 300} 301