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