• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
18 package com.android.mail.browse;
19 
20 import android.content.BroadcastReceiver;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.IntentFilter;
24 import android.content.res.Resources;
25 import android.graphics.Typeface;
26 import androidx.core.text.BidiFormatter;
27 import android.text.SpannableString;
28 import android.text.SpannableStringBuilder;
29 import android.text.Spanned;
30 import android.text.TextUtils;
31 import android.text.style.CharacterStyle;
32 import android.text.style.TextAppearanceSpan;
33 
34 import com.android.mail.R;
35 import com.android.mail.providers.Account;
36 import com.android.mail.providers.Conversation;
37 import com.android.mail.providers.ConversationInfo;
38 import com.android.mail.providers.ParticipantInfo;
39 import com.android.mail.providers.UIProvider;
40 import com.android.mail.utils.ObjectCache;
41 import com.google.common.base.Objects;
42 import com.google.common.collect.Lists;
43 import com.google.common.collect.Maps;
44 
45 import java.util.ArrayList;
46 import java.util.List;
47 import java.util.Map;
48 
49 public 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 
getTypeface(boolean isUnread)99     public static Typeface getTypeface(boolean isUnread) {
100         return isUnread ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT;
101     }
102 
getSenderResources( Context context, final boolean resourceCachingRequired)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 
createMessageInfo(Context context, Conversation conv, final boolean resourceCachingRequired)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 
appendMessageInfo(SpannableStringBuilder sb, CharSequence text, Object span, boolean appendSplitToken, boolean convRead)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 
format(Context context, ConversationInfo conversationInfo, String messageInfo, int maxChars, ArrayList<SpannableString> styledSenders, ArrayList<String> displayableSenderNames, ConversationItemViewModel.SenderAvatarModel senderAvatarModel, Account account, final boolean showToHeader, final boolean resourceCachingRequired)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 
format(Context context, ConversationInfo conversationInfo, String messageInfo, int maxChars, ArrayList<SpannableString> styledSenders, ArrayList<String> displayableSenderNames, ConversationItemViewModel.SenderAvatarModel senderAvatarModel, Account account, final TextAppearanceSpan notificationUnreadStyleSpan, final CharacterStyle notificationReadStyleSpan, final boolean showToHeader, final boolean resourceCachingRequired)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 
handlePriority(int maxChars, String messageInfoString, ConversationInfo conversationInfo, ArrayList<SpannableString> styledSenders, ArrayList<String> displayableSenderNames, ConversationItemViewModel.SenderAvatarModel senderAvatarModel, Account account, final TextAppearanceSpan unreadStyleSpan, final CharacterStyle readStyleSpan, final boolean showToHeader)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 
getMe(boolean useObjectMe)438     static String getMe(boolean useObjectMe) {
439         return useObjectMe ? sMeObjectString : sMeSubjectString;
440     }
441 
getFormattedToHeader()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 
getSingularDraftString(Context context)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 
clearResourceCache()457     private static void clearResourceCache() {
458         sDraftSingularString = null;
459     }
460 }
461