SendersView.java revision d25c753fd348cbd98a148e434134bbe2191aad7d
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.Resources;
25import android.graphics.Typeface;
26import android.text.Spannable;
27import android.text.SpannableString;
28import android.text.SpannableStringBuilder;
29import android.text.TextUtils;
30import android.text.style.CharacterStyle;
31import android.text.style.TextAppearanceSpan;
32import android.text.util.Rfc822Token;
33import android.text.util.Rfc822Tokenizer;
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.providers.UIProvider;
41import com.android.mail.ui.DividedImageCanvas;
42import com.android.mail.utils.ObjectCache;
43import com.google.common.base.Objects;
44import com.google.common.collect.Maps;
45
46import java.util.ArrayList;
47import java.util.Locale;
48import java.util.Map;
49
50import java.util.regex.Pattern;
51
52public class SendersView {
53    public static final int DEFAULT_FORMATTING = 0;
54    public static final int MERGED_FORMATTING = 1;
55    private static final Integer DOES_NOT_EXIST = -5;
56    private static String sSendersSplitToken;
57    public static String SENDERS_VERSION_SEPARATOR = "^**^";
58    public static Pattern SENDERS_VERSION_SEPARATOR_PATTERN = Pattern.compile("\\^\\*\\*\\^");
59    private static CharSequence sDraftSingularString;
60    private static CharSequence sDraftPluralString;
61    private static CharSequence sSendingString;
62    private static String sDraftCountFormatString;
63    private static CharacterStyle sDraftsStyleSpan;
64    private static CharacterStyle sSendingStyleSpan;
65    private static TextAppearanceSpan sUnreadStyleSpan;
66    private static CharacterStyle sReadStyleSpan;
67    private static String sMeString;
68    private static Locale sMeStringLocale;
69    private static String sMessageCountSpacerString;
70    public static CharSequence sElidedString;
71    private static BroadcastReceiver sConfigurationChangedReceiver;
72    private static TextAppearanceSpan sMessageInfoReadStyleSpan;
73    private static TextAppearanceSpan sMessageInfoUnreadStyleSpan;
74
75    // We only want to have at most 2 Priority to length maps.  This will handle the case where
76    // there is a widget installed on the launcher while the user is scrolling in the app
77    private static final int MAX_PRIORITY_LENGTH_MAP_LIST = 2;
78
79    // Cache of priority to length maps.  We can't just use a single instance as it may be
80    // modified from different threads
81    private static final ObjectCache<Map<Integer, Integer>> PRIORITY_LENGTH_MAP_CACHE =
82            new ObjectCache<Map<Integer, Integer>>(
83                    new ObjectCache.Callback<Map<Integer, Integer>>() {
84                        @Override
85                        public Map<Integer, Integer> newInstance() {
86                            return Maps.newHashMap();
87                        }
88                        @Override
89                        public void onObjectReleased(Map<Integer, Integer> object) {
90                            object.clear();
91                        }
92                    }, MAX_PRIORITY_LENGTH_MAP_LIST);
93
94    public static Typeface getTypeface(boolean isUnread) {
95        return isUnread ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT;
96    }
97
98    private static void getSenderResources(Context context) {
99        if (sConfigurationChangedReceiver == null) {
100            sConfigurationChangedReceiver = new BroadcastReceiver() {
101                @Override
102                public void onReceive(Context context, Intent intent) {
103                    sDraftSingularString = null;
104                    getSenderResources(context);
105                }
106            };
107            context.registerReceiver(sConfigurationChangedReceiver, new IntentFilter(
108                    Intent.ACTION_CONFIGURATION_CHANGED));
109        }
110        if (sDraftSingularString == null) {
111            Resources res = context.getResources();
112            sSendersSplitToken = res.getString(R.string.senders_split_token);
113            sElidedString = res.getString(R.string.senders_elided);
114            sDraftSingularString = res.getQuantityText(R.plurals.draft, 1);
115            sDraftPluralString = res.getQuantityText(R.plurals.draft, 2);
116            sDraftCountFormatString = res.getString(R.string.draft_count_format);
117            sMessageInfoUnreadStyleSpan = new TextAppearanceSpan(context,
118                    R.style.MessageInfoUnreadTextAppearance);
119            sMessageInfoReadStyleSpan = new TextAppearanceSpan(context,
120                    R.style.MessageInfoReadTextAppearance);
121            sDraftsStyleSpan = new TextAppearanceSpan(context, R.style.DraftTextAppearance);
122            sUnreadStyleSpan = new TextAppearanceSpan(context, R.style.SendersUnreadTextAppearance);
123            sSendingStyleSpan = new TextAppearanceSpan(context, R.style.SendingTextAppearance);
124            sReadStyleSpan = new TextAppearanceSpan(context, R.style.SendersReadTextAppearance);
125            sMessageCountSpacerString = res.getString(R.string.message_count_spacer);
126            sSendingString = res.getString(R.string.sending);
127        }
128    }
129
130    public static SpannableStringBuilder createMessageInfo(Context context, Conversation conv) {
131        ConversationInfo conversationInfo = conv.conversationInfo;
132        int sendingStatus = conv.sendingState;
133        SpannableStringBuilder messageInfo = new SpannableStringBuilder();
134        boolean hasSenders = false;
135        // This covers the case where the sender is "me" and this is a draft
136        // message, which means this will only run once most of the time.
137        for (MessageInfo m : conversationInfo.messageInfos) {
138            if (!TextUtils.isEmpty(m.sender)) {
139                hasSenders = true;
140                break;
141            }
142        }
143        getSenderResources(context);
144        if (conversationInfo != null) {
145            int count = conversationInfo.messageCount;
146            int draftCount = conversationInfo.draftCount;
147            boolean showSending = sendingStatus == UIProvider.ConversationSendingState.SENDING;
148            if (count > 1) {
149                messageInfo.append(count + "");
150            }
151            messageInfo.setSpan(CharacterStyle.wrap(
152                    conv.read ? sMessageInfoReadStyleSpan : sMessageInfoUnreadStyleSpan),
153                    0, messageInfo.length(), 0);
154            if (draftCount > 0) {
155                // If we are showing a message count or any draft text and there
156                // is at least 1 sender, prepend the sending state text with a
157                // comma.
158                if (hasSenders || count > 1) {
159                    messageInfo.append(sSendersSplitToken);
160                }
161                SpannableStringBuilder draftString = new SpannableStringBuilder();
162                if (draftCount == 1) {
163                    draftString.append(sDraftSingularString);
164                } else {
165                    draftString.append(sDraftPluralString
166                            + String.format(sDraftCountFormatString, draftCount));
167                }
168                draftString.setSpan(CharacterStyle.wrap(sDraftsStyleSpan), 0, draftString.length(),
169                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
170                messageInfo.append(draftString);
171            }
172            if (showSending) {
173                // If we are showing a message count or any draft text, prepend
174                // the sending state text with a comma.
175                if (count > 1 || draftCount > 0) {
176                    messageInfo.append(sSendersSplitToken);
177                }
178                SpannableStringBuilder sending = new SpannableStringBuilder();
179                sending.append(sSendingString);
180                sending.setSpan(sSendingStyleSpan, 0, sending.length(), 0);
181                messageInfo.append(sending);
182            }
183            // Prepend a space if we are showing other message info text.
184            if (count > 1 || (draftCount > 0 && hasSenders) || showSending) {
185                messageInfo = new SpannableStringBuilder(sMessageCountSpacerString)
186                        .append(messageInfo);
187            }
188        }
189        return messageInfo;
190    }
191
192    public static void format(Context context, ConversationInfo conversationInfo,
193            String messageInfo, int maxChars, ArrayList<SpannableString> styledSenders,
194            ArrayList<String> displayableSenderNames, ArrayList<String> displayableSenderEmails,
195            String account) {
196        getSenderResources(context);
197        format(context, conversationInfo, messageInfo, maxChars, styledSenders,
198                displayableSenderNames, displayableSenderEmails, account,
199                sUnreadStyleSpan, sReadStyleSpan);
200    }
201
202    public static void format(Context context, ConversationInfo conversationInfo,
203            String messageInfo, int maxChars, ArrayList<SpannableString> styledSenders,
204            ArrayList<String> displayableSenderNames, ArrayList<String> displayableSenderEmails,
205            String account, final TextAppearanceSpan notificationUnreadStyleSpan,
206            final CharacterStyle notificationReadStyleSpan) {
207        getSenderResources(context);
208        handlePriority(context, maxChars, messageInfo, conversationInfo, styledSenders,
209                displayableSenderNames, displayableSenderEmails, account,
210                notificationUnreadStyleSpan, notificationReadStyleSpan);
211    }
212
213    public static void handlePriority(Context context, int maxChars, String messageInfoString,
214            ConversationInfo conversationInfo, ArrayList<SpannableString> styledSenders,
215            ArrayList<String> displayableSenderNames, ArrayList<String> displayableSenderEmails,
216            String account, final TextAppearanceSpan unreadStyleSpan,
217            final CharacterStyle readStyleSpan) {
218        boolean shouldAddPhotos = displayableSenderEmails != null;
219        int maxPriorityToInclude = -1; // inclusive
220        int numCharsUsed = messageInfoString.length(); // draft, number drafts,
221                                                       // count
222        int numSendersUsed = 0;
223        int numCharsToRemovePerWord = 0;
224        int maxFoundPriority = 0;
225        if (numCharsUsed > maxChars) {
226            numCharsToRemovePerWord = numCharsUsed - maxChars;
227        }
228
229        final Map<Integer, Integer> priorityToLength = PRIORITY_LENGTH_MAP_CACHE.get();
230        try {
231            priorityToLength.clear();
232            int senderLength;
233            for (MessageInfo info : conversationInfo.messageInfos) {
234                senderLength = !TextUtils.isEmpty(info.sender) ? info.sender.length() : 0;
235                priorityToLength.put(info.priority, senderLength);
236                maxFoundPriority = Math.max(maxFoundPriority, info.priority);
237            }
238            while (maxPriorityToInclude < maxFoundPriority) {
239                if (priorityToLength.containsKey(maxPriorityToInclude + 1)) {
240                    int length = numCharsUsed + priorityToLength.get(maxPriorityToInclude + 1);
241                    if (numCharsUsed > 0)
242                        length += 2;
243                    // We must show at least two senders if they exist. If we don't
244                    // have space for both
245                    // then we will truncate names.
246                    if (length > maxChars && numSendersUsed >= 2) {
247                        break;
248                    }
249                    numCharsUsed = length;
250                    numSendersUsed++;
251                }
252                maxPriorityToInclude++;
253            }
254        } finally {
255            PRIORITY_LENGTH_MAP_CACHE.release(priorityToLength);
256        }
257        // We want to include this entry if
258        // 1) The onlyShowUnread flags is not set
259        // 2) The above flag is set, and the message is unread
260        MessageInfo currentMessage;
261        SpannableString spannableDisplay;
262        String nameString;
263        CharacterStyle style;
264        boolean appendedElided = false;
265        Map<String, Integer> displayHash = Maps.newHashMap();
266        String firstDisplayableSenderEmail = null;
267        String firstDisplayableSender = null;
268        for (int i = 0; i < conversationInfo.messageInfos.size(); i++) {
269            currentMessage = conversationInfo.messageInfos.get(i);
270            nameString = !TextUtils.isEmpty(currentMessage.sender) ? currentMessage.sender : "";
271            if (nameString.length() == 0) {
272                nameString = getMe(context);
273            }
274            if (numCharsToRemovePerWord != 0) {
275                nameString = nameString.substring(0,
276                        Math.max(nameString.length() - numCharsToRemovePerWord, 0));
277            }
278            final int priority = currentMessage.priority;
279            style = !currentMessage.read ? getWrappedStyleSpan(unreadStyleSpan)
280                    : getWrappedStyleSpan(readStyleSpan);
281            if (priority <= maxPriorityToInclude) {
282                spannableDisplay = new SpannableString(nameString);
283                // Don't duplicate senders; leave the first instance, unless the
284                // current instance is also unread.
285                int oldPos = displayHash.containsKey(currentMessage.sender) ? displayHash
286                        .get(currentMessage.sender) : DOES_NOT_EXIST;
287                // If this sender doesn't exist OR the current message is
288                // unread, add the sender.
289                if (oldPos == DOES_NOT_EXIST || !currentMessage.read) {
290                    // If the sender entry already existed, and is right next to the
291                    // current sender, remove the old entry.
292                    if (oldPos != DOES_NOT_EXIST && i > 0 && oldPos == i - 1
293                            && oldPos < styledSenders.size()) {
294                        // Remove the old one!
295                        styledSenders.set(oldPos, null);
296                        if (shouldAddPhotos && !TextUtils.isEmpty(currentMessage.senderEmail)) {
297                            displayableSenderEmails.remove(currentMessage.senderEmail);
298                            displayableSenderNames.remove(currentMessage.sender);
299                        }
300                    }
301                    displayHash.put(currentMessage.sender, i);
302                    spannableDisplay.setSpan(style, 0, spannableDisplay.length(), 0);
303                    styledSenders.add(spannableDisplay);
304                }
305            } else {
306                if (!appendedElided) {
307                    spannableDisplay = new SpannableString(sElidedString);
308                    spannableDisplay.setSpan(style, 0, spannableDisplay.length(), 0);
309                    appendedElided = true;
310                    styledSenders.add(spannableDisplay);
311                }
312            }
313            if (shouldAddPhotos) {
314                String senderEmail = TextUtils.isEmpty(currentMessage.sender) ?
315                        account :
316                            TextUtils.isEmpty(currentMessage.senderEmail) ?
317                                    currentMessage.sender : currentMessage.senderEmail;
318                if (i == 0) {
319                    // Always add the first sender!
320                    firstDisplayableSenderEmail = senderEmail;
321                    firstDisplayableSender = currentMessage.sender;
322                } else {
323                    if (!Objects.equal(firstDisplayableSenderEmail, senderEmail)) {
324                        int indexOf = displayableSenderEmails.indexOf(senderEmail);
325                        if (indexOf > -1) {
326                            displayableSenderEmails.remove(indexOf);
327                            displayableSenderNames.remove(indexOf);
328                        }
329                        displayableSenderEmails.add(senderEmail);
330                        displayableSenderNames.add(currentMessage.sender);
331                        if (displayableSenderEmails.size() > DividedImageCanvas.MAX_DIVISIONS) {
332                            displayableSenderEmails.remove(0);
333                            displayableSenderNames.remove(0);
334                        }
335                    }
336                }
337            }
338        }
339        if (shouldAddPhotos && !TextUtils.isEmpty(firstDisplayableSenderEmail)) {
340            if (displayableSenderEmails.size() < DividedImageCanvas.MAX_DIVISIONS) {
341                displayableSenderEmails.add(0, firstDisplayableSenderEmail);
342                displayableSenderNames.add(0, firstDisplayableSender);
343            } else {
344                displayableSenderEmails.set(0, firstDisplayableSenderEmail);
345                displayableSenderNames.set(0, firstDisplayableSender);
346            }
347        }
348    }
349
350    private static CharacterStyle getWrappedStyleSpan(final CharacterStyle characterStyle) {
351        return CharacterStyle.wrap(characterStyle);
352    }
353
354    static String getMe(Context context) {
355        final Resources resources = context.getResources();
356        final Locale locale = resources.getConfiguration().locale;
357
358        if (sMeString == null || !locale.equals(sMeStringLocale)) {
359            sMeString = resources.getString(R.string.me);
360            sMeStringLocale = locale;
361        }
362        return sMeString;
363    }
364
365    private static void formatDefault(ConversationItemViewModel header, String sendersString,
366            Context context, final CharacterStyle readStyleSpan) {
367        getSenderResources(context);
368        // Clear any existing sender fragments; we must re-make all of them.
369        header.senderFragments.clear();
370        String[] senders = TextUtils.split(sendersString, Address.ADDRESS_DELIMETER);
371        String[] namesOnly = new String[senders.length];
372        Rfc822Token[] senderTokens;
373        String display;
374        for (int i = 0; i < senders.length; i++) {
375            senderTokens = Rfc822Tokenizer.tokenize(senders[i]);
376            if (senderTokens != null && senderTokens.length > 0) {
377                display = senderTokens[0].getName();
378                if (TextUtils.isEmpty(display)) {
379                    display = senderTokens[0].getAddress();
380                }
381                namesOnly[i] = display;
382            }
383        }
384        generateSenderFragments(header, namesOnly, readStyleSpan);
385    }
386
387    private static void generateSenderFragments(ConversationItemViewModel header, String[] names,
388            final CharacterStyle readStyleSpan) {
389        header.sendersText = TextUtils.join(Address.ADDRESS_DELIMETER + " ", names);
390        header.addSenderFragment(0, header.sendersText.length(), getWrappedStyleSpan(readStyleSpan),
391                true);
392    }
393
394    public static void formatSenders(ConversationItemViewModel header, Context context) {
395        getSenderResources(context);
396        formatSenders(header, context, sReadStyleSpan);
397    }
398
399    public static void formatSenders(ConversationItemViewModel header, Context context,
400            final CharacterStyle readStyleSpan) {
401        formatDefault(header, header.conversation.senders, context, readStyleSpan);
402    }
403}
404