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