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.SpannableString;
28import android.text.SpannableStringBuilder;
29import android.text.Spanned;
30import android.text.TextUtils;
31import android.text.style.CharacterStyle;
32import android.text.style.TextAppearanceSpan;
33
34import com.android.mail.R;
35import com.android.mail.providers.Account;
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.utils.ObjectCache;
41import com.google.common.base.Objects;
42import com.google.common.collect.Lists;
43import com.google.common.collect.Maps;
44
45import java.util.ArrayList;
46import java.util.List;
47import java.util.Map;
48
49public class SendersView {
50    /** The maximum number of senders to display for a given conversation */
51    private static final int MAX_SENDER_COUNT = 4;
52
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    private static CharSequence sDraftSingularString;
59    private static CharSequence sDraftPluralString;
60    private static CharSequence sSendingString;
61    private static CharSequence sRetryingString;
62    private static CharSequence sFailedString;
63    private static String sDraftCountFormatString;
64    private static CharacterStyle sDraftsStyleSpan;
65    private static CharacterStyle sSendingStyleSpan;
66    private static CharacterStyle sRetryingStyleSpan;
67    private static CharacterStyle sFailedStyleSpan;
68    private static TextAppearanceSpan sUnreadStyleSpan;
69    private static CharacterStyle sReadStyleSpan;
70    private static String sMeSubjectString;
71    private static String sMeObjectString;
72    private static String sToHeaderString;
73    private static String sMessageCountSpacerString;
74    public static CharSequence sElidedString;
75    private static BroadcastReceiver sConfigurationChangedReceiver;
76    private static TextAppearanceSpan sMessageInfoReadStyleSpan;
77    private static TextAppearanceSpan sMessageInfoUnreadStyleSpan;
78    private static BidiFormatter sBidiFormatter;
79
80    // We only want to have at most 2 Priority to length maps.  This will handle the case where
81    // there is a widget installed on the launcher while the user is scrolling in the app
82    private static final int MAX_PRIORITY_LENGTH_MAP_LIST = 2;
83
84    // Cache of priority to length maps.  We can't just use a single instance as it may be
85    // modified from different threads
86    private static final ObjectCache<Map<Integer, Integer>> PRIORITY_LENGTH_MAP_CACHE =
87            new ObjectCache<Map<Integer, Integer>>(
88                    new ObjectCache.Callback<Map<Integer, Integer>>() {
89                        @Override
90                        public Map<Integer, Integer> newInstance() {
91                            return Maps.newHashMap();
92                        }
93                        @Override
94                        public void onObjectReleased(Map<Integer, Integer> object) {
95                            object.clear();
96                        }
97                    }, MAX_PRIORITY_LENGTH_MAP_LIST);
98
99    public static Typeface getTypeface(boolean isUnread) {
100        return isUnread ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT;
101    }
102
103    private static synchronized void getSenderResources(
104            Context context, final boolean resourceCachingRequired) {
105        if (sConfigurationChangedReceiver == null && resourceCachingRequired) {
106            sConfigurationChangedReceiver = new BroadcastReceiver() {
107                @Override
108                public void onReceive(Context context, Intent intent) {
109                    sDraftSingularString = null;
110                    getSenderResources(context, true);
111                }
112            };
113            context.registerReceiver(sConfigurationChangedReceiver, new IntentFilter(
114                    Intent.ACTION_CONFIGURATION_CHANGED));
115        }
116        if (sDraftSingularString == null) {
117            Resources res = context.getResources();
118            sSendersSplitToken = res.getString(R.string.senders_split_token);
119            sElidedString = res.getString(R.string.senders_elided);
120            sDraftSingularString = res.getQuantityText(R.plurals.draft, 1);
121            sDraftPluralString = res.getQuantityText(R.plurals.draft, 2);
122            sDraftCountFormatString = res.getString(R.string.draft_count_format);
123            sMeSubjectString = res.getString(R.string.me_subject_pronoun);
124            sMeObjectString = res.getString(R.string.me_object_pronoun);
125            sToHeaderString = res.getString(R.string.to_heading);
126            sMessageInfoUnreadStyleSpan = new TextAppearanceSpan(context,
127                    R.style.MessageInfoUnreadTextAppearance);
128            sMessageInfoReadStyleSpan = new TextAppearanceSpan(context,
129                    R.style.MessageInfoReadTextAppearance);
130            sDraftsStyleSpan = new TextAppearanceSpan(context, R.style.DraftTextAppearance);
131            sUnreadStyleSpan = new TextAppearanceSpan(context, R.style.SendersAppearanceUnreadStyle);
132            sSendingStyleSpan = new TextAppearanceSpan(context, R.style.SendingTextAppearance);
133            sRetryingStyleSpan = new TextAppearanceSpan(context, R.style.RetryingTextAppearance);
134            sFailedStyleSpan = new TextAppearanceSpan(context, R.style.FailedTextAppearance);
135            sReadStyleSpan = new TextAppearanceSpan(context, R.style.SendersAppearanceReadStyle);
136            sMessageCountSpacerString = res.getString(R.string.message_count_spacer);
137            sSendingString = res.getString(R.string.sending);
138            sRetryingString = res.getString(R.string.message_retrying);
139            sFailedString = res.getString(R.string.message_failed);
140            sBidiFormatter = BidiFormatter.getInstance();
141        }
142    }
143
144    public static SpannableStringBuilder createMessageInfo(Context context, Conversation conv,
145            final boolean resourceCachingRequired) {
146        SpannableStringBuilder messageInfo = new SpannableStringBuilder();
147
148        try {
149            final ConversationInfo conversationInfo = conv.conversationInfo;
150            final int sendingStatus = conv.sendingState;
151            boolean hasSenders = false;
152            // This covers the case where the sender is "me" and this is a draft
153            // message, which means this will only run once most of the time.
154            for (ParticipantInfo p : conversationInfo.participantInfos) {
155                if (!TextUtils.isEmpty(p.name)) {
156                    hasSenders = true;
157                    break;
158                }
159            }
160            getSenderResources(context, resourceCachingRequired);
161            final int count = conversationInfo.messageCount;
162            final int draftCount = conversationInfo.draftCount;
163            if (count > 1) {
164                appendMessageInfo(messageInfo, Integer.toString(count), CharacterStyle.wrap(
165                        conv.read ? sMessageInfoReadStyleSpan : sMessageInfoUnreadStyleSpan),
166                        false, conv.read);
167            }
168
169            boolean appendSplitToken = hasSenders || count > 1;
170            if (draftCount > 0) {
171                final CharSequence draftText;
172                if (draftCount == 1) {
173                    draftText = sDraftSingularString;
174                } else {
175                    draftText = sDraftPluralString +
176                            String.format(sDraftCountFormatString, draftCount);
177                }
178
179                appendMessageInfo(messageInfo, draftText, sDraftsStyleSpan, appendSplitToken,
180                        conv.read);
181            }
182
183            final boolean showState = sendingStatus == UIProvider.ConversationSendingState.SENDING ||
184                    sendingStatus == UIProvider.ConversationSendingState.RETRYING ||
185                    sendingStatus == UIProvider.ConversationSendingState.SEND_ERROR;
186            if (showState) {
187                appendSplitToken |= draftCount > 0;
188
189                final CharSequence statusText;
190                final Object span;
191                if (sendingStatus == UIProvider.ConversationSendingState.SENDING) {
192                    statusText = sSendingString;
193                    span = sSendingStyleSpan;
194                } else if (sendingStatus == UIProvider.ConversationSendingState.RETRYING) {
195                    statusText = sSendingString;
196                    span = sSendingStyleSpan;
197                } else {
198                    statusText = sFailedString;
199                    span = sFailedStyleSpan;
200                }
201
202                appendMessageInfo(messageInfo, statusText, span, appendSplitToken, conv.read);
203            }
204
205            // Prepend a space if we are showing other message info text.
206            if (count > 1 || (draftCount > 0 && hasSenders) || showState) {
207                messageInfo.insert(0, sMessageCountSpacerString);
208            }
209        } finally {
210            if (!resourceCachingRequired) {
211                clearResourceCache();
212            }
213        }
214
215        return messageInfo;
216    }
217
218    private static void appendMessageInfo(SpannableStringBuilder sb, CharSequence text,
219            Object span, boolean appendSplitToken, boolean convRead) {
220        int startIndex = sb.length();
221        if (appendSplitToken) {
222            sb.append(sSendersSplitToken);
223            sb.setSpan(CharacterStyle.wrap(convRead ?
224                    sMessageInfoReadStyleSpan : sMessageInfoUnreadStyleSpan),
225                    startIndex, sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
226        }
227
228        startIndex = sb.length();
229        sb.append(text);
230        sb.setSpan(span, startIndex, sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
231    }
232
233    public static void format(Context context, ConversationInfo conversationInfo,
234            String messageInfo, int maxChars, ArrayList<SpannableString> styledSenders,
235            ArrayList<String> displayableSenderNames,
236            ConversationItemViewModel.SenderAvatarModel senderAvatarModel,
237            Account account, final boolean showToHeader, final boolean resourceCachingRequired) {
238        try {
239            getSenderResources(context, resourceCachingRequired);
240            format(context, conversationInfo, messageInfo, maxChars, styledSenders,
241                    displayableSenderNames, senderAvatarModel, account,
242                    sUnreadStyleSpan, sReadStyleSpan, showToHeader, resourceCachingRequired);
243        } finally {
244            if (!resourceCachingRequired) {
245                clearResourceCache();
246            }
247        }
248    }
249
250    public static void format(Context context, ConversationInfo conversationInfo,
251            String messageInfo, int maxChars, ArrayList<SpannableString> styledSenders,
252            ArrayList<String> displayableSenderNames,
253            ConversationItemViewModel.SenderAvatarModel senderAvatarModel,
254            Account account, final TextAppearanceSpan notificationUnreadStyleSpan,
255            final CharacterStyle notificationReadStyleSpan, final boolean showToHeader,
256            final boolean resourceCachingRequired) {
257        try {
258            getSenderResources(context, resourceCachingRequired);
259            handlePriority(maxChars, messageInfo, conversationInfo, styledSenders,
260                    displayableSenderNames, senderAvatarModel, account,
261                    notificationUnreadStyleSpan, notificationReadStyleSpan, showToHeader);
262        } finally {
263            if (!resourceCachingRequired) {
264                clearResourceCache();
265            }
266        }
267    }
268
269    private static void handlePriority(int maxChars, String messageInfoString,
270            ConversationInfo conversationInfo, ArrayList<SpannableString> styledSenders,
271            ArrayList<String> displayableSenderNames,
272            ConversationItemViewModel.SenderAvatarModel senderAvatarModel,
273            Account account, final TextAppearanceSpan unreadStyleSpan,
274            final CharacterStyle readStyleSpan, final boolean showToHeader) {
275        final boolean shouldSelectSenders = displayableSenderNames != null;
276        final boolean shouldSelectAvatar = senderAvatarModel != null;
277        int maxPriorityToInclude = -1; // inclusive
278        int numCharsUsed = messageInfoString.length(); // draft, number drafts,
279                                                       // count
280        int numSendersUsed = 0;
281        int numCharsToRemovePerWord = 0;
282        int maxFoundPriority = 0;
283        if (numCharsUsed > maxChars) {
284            numCharsToRemovePerWord = numCharsUsed - maxChars;
285        }
286
287        final Map<Integer, Integer> priorityToLength = PRIORITY_LENGTH_MAP_CACHE.get();
288        try {
289            priorityToLength.clear();
290            int senderLength;
291            for (ParticipantInfo info : conversationInfo.participantInfos) {
292                final String senderName = info.name;
293                senderLength = !TextUtils.isEmpty(senderName) ? senderName.length() : 0;
294                priorityToLength.put(info.priority, senderLength);
295                maxFoundPriority = Math.max(maxFoundPriority, info.priority);
296            }
297            while (maxPriorityToInclude < maxFoundPriority) {
298                if (priorityToLength.containsKey(maxPriorityToInclude + 1)) {
299                    int length = numCharsUsed + priorityToLength.get(maxPriorityToInclude + 1);
300                    if (numCharsUsed > 0)
301                        length += 2;
302                    // We must show at least two senders if they exist. If we don't
303                    // have space for both
304                    // then we will truncate names.
305                    if (length > maxChars && numSendersUsed >= 2) {
306                        break;
307                    }
308                    numCharsUsed = length;
309                    numSendersUsed++;
310                }
311                maxPriorityToInclude++;
312            }
313        } finally {
314            PRIORITY_LENGTH_MAP_CACHE.release(priorityToLength);
315        }
316
317        SpannableString spannableDisplay;
318        boolean appendedElided = false;
319        final Map<String, Integer> displayHash = Maps.newHashMap();
320        final List<String> senderEmails = Lists.newArrayListWithExpectedSize(MAX_SENDER_COUNT);
321        String firstSenderEmail = null;
322        String firstSenderName = null;
323        for (int i = 0; i < conversationInfo.participantInfos.size(); i++) {
324            final ParticipantInfo currentParticipant = conversationInfo.participantInfos.get(i);
325            final String currentEmail = currentParticipant.email;
326
327            final String currentName = currentParticipant.name;
328            String nameString = !TextUtils.isEmpty(currentName) ? currentName : "";
329            if (nameString.length() == 0) {
330                // if we're showing the To: header, show the object version of me.
331                nameString = getMe(showToHeader /* useObjectMe */);
332            }
333            if (numCharsToRemovePerWord != 0) {
334                nameString = nameString.substring(0,
335                        Math.max(nameString.length() - numCharsToRemovePerWord, 0));
336            }
337
338            final int priority = currentParticipant.priority;
339            final CharacterStyle style = CharacterStyle.wrap(currentParticipant.readConversation ?
340                    readStyleSpan : unreadStyleSpan);
341            if (priority <= maxPriorityToInclude) {
342                spannableDisplay = new SpannableString(sBidiFormatter.unicodeWrap(nameString));
343                // Don't duplicate senders; leave the first instance, unless the
344                // current instance is also unread.
345                int oldPos = displayHash.containsKey(currentName) ? displayHash
346                        .get(currentName) : DOES_NOT_EXIST;
347                // If this sender doesn't exist OR the current message is
348                // unread, add the sender.
349                if (oldPos == DOES_NOT_EXIST || !currentParticipant.readConversation) {
350                    // If the sender entry already existed, and is right next to the
351                    // current sender, remove the old entry.
352                    if (oldPos != DOES_NOT_EXIST && i > 0 && oldPos == i - 1
353                            && oldPos < styledSenders.size()) {
354                        // Remove the old one!
355                        styledSenders.set(oldPos, null);
356                        if (shouldSelectSenders && !TextUtils.isEmpty(currentEmail)) {
357                            senderEmails.remove(currentEmail);
358                            displayableSenderNames.remove(currentName);
359                        }
360                    }
361                    displayHash.put(currentName, i);
362                    spannableDisplay.setSpan(style, 0, spannableDisplay.length(), 0);
363                    styledSenders.add(spannableDisplay);
364                }
365            } else {
366                if (!appendedElided) {
367                    spannableDisplay = new SpannableString(sElidedString);
368                    spannableDisplay.setSpan(style, 0, spannableDisplay.length(), 0);
369                    appendedElided = true;
370                    styledSenders.add(spannableDisplay);
371                }
372            }
373
374            final String senderEmail = TextUtils.isEmpty(currentName) ? account.getEmailAddress() :
375                    TextUtils.isEmpty(currentEmail) ? currentName : currentEmail;
376
377            if (shouldSelectSenders) {
378                if (i == 0) {
379                    // Always add the first sender!
380                    firstSenderEmail = senderEmail;
381                    firstSenderName = currentName;
382                } else {
383                    if (!Objects.equal(firstSenderEmail, senderEmail)) {
384                        int indexOf = senderEmails.indexOf(senderEmail);
385                        if (indexOf > -1) {
386                            senderEmails.remove(indexOf);
387                            displayableSenderNames.remove(indexOf);
388                        }
389                        senderEmails.add(senderEmail);
390                        displayableSenderNames.add(currentName);
391                        if (senderEmails.size() > MAX_SENDER_COUNT) {
392                            senderEmails.remove(0);
393                            displayableSenderNames.remove(0);
394                        }
395                    }
396                }
397            }
398
399            // if the corresponding message from this participant is unread and no sender avatar
400            // is yet chosen, choose this one
401            if (shouldSelectAvatar && senderAvatarModel.isNotPopulated() &&
402                    !currentParticipant.readConversation) {
403                senderAvatarModel.populate(currentName, senderEmail);
404            }
405        }
406
407        // always add the first sender to the display
408        if (shouldSelectSenders && !TextUtils.isEmpty(firstSenderEmail)) {
409            if (displayableSenderNames.size() < MAX_SENDER_COUNT) {
410                displayableSenderNames.add(0, firstSenderName);
411            } else {
412                displayableSenderNames.set(0, firstSenderName);
413            }
414        }
415
416        // if all messages in the thread were read, we must search for an appropriate avatar
417        if (shouldSelectAvatar && senderAvatarModel.isNotPopulated()) {
418            // search for the last sender that is not the current account
419            for (int i = conversationInfo.participantInfos.size() - 1; i >= 0; i--) {
420                final ParticipantInfo participant = conversationInfo.participantInfos.get(i);
421                // empty name implies it is the current account and should not be chosen
422                if (!TextUtils.isEmpty(participant.name)) {
423                    // use the participant name in place of unusable email addresses
424                    final String senderEmail = TextUtils.isEmpty(participant.email) ?
425                            participant.name : participant.email;
426                    senderAvatarModel.populate(participant.name, senderEmail);
427                    break;
428                }
429            }
430
431            // if we still don't have an avatar, the account is emailing itself
432            if (senderAvatarModel.isNotPopulated()) {
433                senderAvatarModel.populate(account.getDisplayName(), account.getEmailAddress());
434            }
435        }
436    }
437
438    static String getMe(boolean useObjectMe) {
439        return useObjectMe ? sMeObjectString : sMeSubjectString;
440    }
441
442    public static SpannableString getFormattedToHeader() {
443        final SpannableString formattedToHeader = new SpannableString(sToHeaderString);
444        final CharacterStyle readStyle = CharacterStyle.wrap(sReadStyleSpan);
445        formattedToHeader.setSpan(readStyle, 0, formattedToHeader.length(), 0);
446        return formattedToHeader;
447    }
448
449    public static SpannableString getSingularDraftString(Context context) {
450        getSenderResources(context, true /* resourceCachingRequired */);
451        final SpannableString formattedDraftString = new SpannableString(sDraftSingularString);
452        final CharacterStyle readStyle = CharacterStyle.wrap(sDraftsStyleSpan);
453        formattedDraftString.setSpan(readStyle, 0, formattedDraftString.length(), 0);
454        return formattedDraftString;
455    }
456
457    private static void clearResourceCache() {
458        sDraftSingularString = null;
459    }
460}
461