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