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