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