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