SendersView.java revision c3efca18f09904f4ce39395169559c5d82bd3d06
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.Context; 21import android.content.res.Resources; 22import android.graphics.Typeface; 23import android.text.Html; 24import android.text.SpannableString; 25import android.text.SpannableStringBuilder; 26import android.text.TextUtils; 27import android.text.style.CharacterStyle; 28import android.text.style.ForegroundColorSpan; 29import android.text.style.StyleSpan; 30import android.text.util.Rfc822Token; 31import android.text.util.Rfc822Tokenizer; 32import android.util.AttributeSet; 33import android.widget.TextView; 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.utils.Utils; 41import com.google.common.annotations.VisibleForTesting; 42 43import java.util.ArrayList; 44import java.util.HashMap; 45import java.util.regex.Pattern; 46 47public class SendersView extends TextView { 48 public static final int DEFAULT_FORMATTING = 0; 49 public static final int MERGED_FORMATTING = 1; 50 private static String SENDERS_SPLIT_TOKEN; 51 public static String SENDERS_VERSION_SEPARATOR = "^**^"; 52 CharacterStyle sNormalTextStyle = new StyleSpan(Typeface.NORMAL); 53 public static Pattern SENDERS_VERSION_SEPARATOR_PATTERN = Pattern.compile("\\^\\*\\*\\^"); 54 private int mFormatVersion = -1; 55 private ForegroundColorSpan sLightTextStyle; 56 private int DRAFT_TEXT_COLOR; 57 private int LIGHT_TEXT_COLOR; 58 private static StyleSpan sUnreadStyleSpan; 59 private static StyleSpan sReadStyleSpan; 60 private static String sMeString; 61 62 public SendersView(Context context) { 63 this(context, null); 64 } 65 66 public SendersView(Context context, AttributeSet attrs) { 67 this(context, attrs, -1); 68 } 69 70 public SendersView(Context context, AttributeSet attrs, int defStyle) { 71 super(context, attrs, defStyle); 72 Resources res = context.getResources(); 73 LIGHT_TEXT_COLOR = res.getColor(R.color.light_text_color); 74 DRAFT_TEXT_COLOR = res.getColor(R.color.drafts); 75 sLightTextStyle = new ForegroundColorSpan(LIGHT_TEXT_COLOR); 76 SENDERS_SPLIT_TOKEN = res.getString(R.string.senders_split_token); 77 } 78 79 public Typeface getTypeface(boolean isUnread) { 80 return mFormatVersion == DEFAULT_FORMATTING ? isUnread ? Typeface.DEFAULT_BOLD 81 : Typeface.DEFAULT : Typeface.DEFAULT; 82 } 83 84 public void formatSenders(ConversationItemViewModel header, boolean isUnread, int mode) { 85 if (TextUtils.isEmpty(header.conversation.senders)) { 86 return; 87 } 88 SendersInfo info = new SendersInfo(header.conversation.senders); 89 mFormatVersion = info.version; 90 switch (mFormatVersion) { 91 case MERGED_FORMATTING: 92 formatMerged(header, info.text, isUnread, mode); 93 break; 94 case DEFAULT_FORMATTING: 95 default: 96 formatDefault(header, info.text); 97 break; 98 } 99 } 100 101 @VisibleForTesting 102 public static SpannableString[] format(Context context, ConversationInfo conversationInfo) { 103 HashMap<String, Integer> displayHash = new HashMap<String, Integer>(); 104 ArrayList<SpannableString> displays = new ArrayList<SpannableString>(); 105 String display; 106 SpannableString spannableDisplay; 107 String sender; 108 CharacterStyle style; 109 MessageInfo currentMessage; 110 for (int i = 0; i < conversationInfo.messageCount; i++) { 111 currentMessage = conversationInfo.messageInfos.get(i); 112 sender = currentMessage.sender; 113 if (TextUtils.isEmpty(sender)) { 114 sender = getMe(context); 115 } else { 116 sender = Html.fromHtml(sender).toString(); 117 } 118 display = parseSender(sender); 119 spannableDisplay = new SpannableString(display); 120 style = !currentMessage.read ? getUnreadStyleSpan() : getReadStyleSpan(); 121 spannableDisplay.setSpan(style, 0, spannableDisplay.length(), 0); 122 if (displayHash.containsKey(display)) { 123 displays.remove(displayHash.get(display).intValue()); 124 } 125 displayHash.put(display, i); 126 displays.add(spannableDisplay); 127 } 128 return displays.toArray(new SpannableString[displays.size()]); 129 } 130 131 private static CharacterStyle getUnreadStyleSpan() { 132 if (sUnreadStyleSpan == null) { 133 sUnreadStyleSpan = new StyleSpan(Typeface.BOLD); 134 } 135 return CharacterStyle.wrap(sUnreadStyleSpan); 136 } 137 138 private static CharacterStyle getReadStyleSpan() { 139 if (sReadStyleSpan == null) { 140 sReadStyleSpan = new StyleSpan(Typeface.NORMAL); 141 } 142 return CharacterStyle.wrap(sReadStyleSpan); 143 } 144 145 private static String getMe(Context context) { 146 if (sMeString == null) { 147 sMeString = context.getResources().getString(R.string.me); 148 } 149 return sMeString; 150 } 151 152 private static String parseSender(String sender) { 153 Rfc822Token[] senderTokens = Rfc822Tokenizer.tokenize(sender); 154 String name; 155 if (senderTokens != null && senderTokens.length > 0) { 156 name = senderTokens[0].getName(); 157 if (TextUtils.isEmpty(name)) { 158 name = senderTokens[0].getAddress(); 159 } 160 return name; 161 } 162 return sender; 163 } 164 165 private void formatDefault(ConversationItemViewModel header, String sendersString) { 166 String[] senders = TextUtils.split(sendersString, Address.ADDRESS_DELIMETER); 167 String[] namesOnly = new String[senders.length]; 168 Rfc822Token[] senderTokens; 169 String display; 170 for (int i = 0; i < senders.length; i++) { 171 senderTokens = Rfc822Tokenizer.tokenize(senders[i]); 172 if (senderTokens != null && senderTokens.length > 0) { 173 display = senderTokens[0].getName(); 174 if (TextUtils.isEmpty(display)) { 175 display = senderTokens[0].getAddress(); 176 } 177 namesOnly[i] = display; 178 } 179 } 180 generateSenderFragments(header, namesOnly); 181 } 182 183 private void generateSenderFragments(ConversationItemViewModel header, String[] names) { 184 header.sendersText = TextUtils.join(Address.ADDRESS_DELIMETER + " ", names); 185 header.addSenderFragment(0, header.sendersText.length(), sNormalTextStyle, true); 186 } 187 188 private void formatMerged(ConversationItemViewModel header, String sendersString, 189 boolean isUnread, int mode) { 190 SpannableStringBuilder sendersBuilder = new SpannableStringBuilder(); 191 SpannableStringBuilder statusBuilder = new SpannableStringBuilder(); 192 Utils.getStyledSenderSnippet(getContext(), sendersString, sendersBuilder, 193 statusBuilder, ConversationItemViewCoordinates.getSubjectLength(getContext(), mode, 194 header.folderDisplayer.hasVisibleFolders(), 195 header.conversation.hasAttachments), false, false, header.hasDraftMessage); 196 header.sendersText = sendersBuilder.toString(); 197 198 CharacterStyle[] spans = sendersBuilder.getSpans(0, sendersBuilder.length(), 199 CharacterStyle.class); 200 header.clearSenderFragments(); 201 int lastPosition = 0; 202 CharacterStyle style = sNormalTextStyle; 203 if (spans != null) { 204 for (CharacterStyle span : spans) { 205 style = span; 206 int start = sendersBuilder.getSpanStart(style); 207 int end = sendersBuilder.getSpanEnd(style); 208 if (start > lastPosition) { 209 header.addSenderFragment(lastPosition, start, sNormalTextStyle, false); 210 } 211 // From instructions won't be updated until the next sync. So we 212 // have to override the text style here to be consistent with 213 // the background color. 214 if (isUnread) { 215 header.addSenderFragment(start, end, style, false); 216 } else { 217 header.addSenderFragment(start, end, sNormalTextStyle, false); 218 } 219 lastPosition = end; 220 } 221 } 222 if (lastPosition < sendersBuilder.length()) { 223 style = sLightTextStyle; 224 header.addSenderFragment(lastPosition, sendersBuilder.length(), style, true); 225 } 226 if (statusBuilder.length() > 0) { 227 if (header.sendersText.length() > 0) { 228 header.sendersText = header.sendersText.concat(SENDERS_SPLIT_TOKEN); 229 230 // Extend the last fragment to include the comma. 231 int lastIndex = header.senderFragments.size() - 1; 232 int start = header.senderFragments.get(lastIndex).start; 233 int end = header.senderFragments.get(lastIndex).end + 2; 234 style = header.senderFragments.get(lastIndex).style; 235 236 // The new fragment is only fixed if the previous fragment 237 // is fixed. 238 boolean isFixed = header.senderFragments.get(lastIndex).isFixed; 239 240 // Remove the old fragment. 241 header.senderFragments.remove(lastIndex); 242 243 // Add new fragment. 244 header.addSenderFragment(start, end, style, isFixed); 245 } 246 int pos = header.sendersText.length(); 247 header.sendersText = header.sendersText.concat(statusBuilder.toString()); 248 header.addSenderFragment(pos, header.sendersText.length(), new ForegroundColorSpan( 249 DRAFT_TEXT_COLOR), true); 250 } 251 } 252 253 public static class SendersInfo { 254 public int version; 255 public String text; 256 257 public SendersInfo(String toParse) { 258 if (TextUtils.isEmpty(toParse)) { 259 version = 0; 260 text = ""; 261 } else { 262 String[] splits = TextUtils.split(toParse, SENDERS_VERSION_SEPARATOR_PATTERN); 263 if (splits == null || splits.length < 2) { 264 version = SendersView.DEFAULT_FORMATTING; 265 text = toParse; 266 } else { 267 version = Integer.parseInt(splits[0]); 268 text = splits[1]; 269 } 270 } 271 } 272 } 273} 274