• 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 android.support.v4.text.BidiFormatter;
27 import android.text.Spannable;
28 import android.text.SpannableString;
29 import android.text.SpannableStringBuilder;
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.Conversation;
36 import com.android.mail.providers.ConversationInfo;
37 import com.android.mail.providers.ParticipantInfo;
38 import com.android.mail.providers.UIProvider;
39 import com.android.mail.ui.DividedImageCanvas;
40 import com.android.mail.utils.ObjectCache;
41 import com.google.common.base.Objects;
42 import com.google.common.collect.Maps;
43 
44 import java.util.ArrayList;
45 import java.util.Map;
46 
47 public 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 
getTypeface(boolean isUnread)94     public static Typeface getTypeface(boolean isUnread) {
95         return isUnread ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT;
96     }
97 
getSenderResources( Context context, final boolean resourceCachingRequired)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 
createMessageInfo(Context context, Conversation conv, final boolean resourceCachingRequired)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 
format(Context context, ConversationInfo conversationInfo, String messageInfo, int maxChars, ArrayList<SpannableString> styledSenders, ArrayList<String> displayableSenderNames, ArrayList<String> displayableSenderEmails, String account, final boolean showToHeader, final boolean resourceCachingRequired)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 
format(Context context, ConversationInfo conversationInfo, String messageInfo, int maxChars, ArrayList<SpannableString> styledSenders, ArrayList<String> displayableSenderNames, ArrayList<String> displayableSenderEmails, String account, final TextAppearanceSpan notificationUnreadStyleSpan, final CharacterStyle notificationReadStyleSpan, final boolean showToHeader, final boolean resourceCachingRequired)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 
handlePriority(int maxChars, String messageInfoString, ConversationInfo conversationInfo, ArrayList<SpannableString> styledSenders, ArrayList<String> displayableSenderNames, ArrayList<String> displayableSenderEmails, String account, final TextAppearanceSpan unreadStyleSpan, final CharacterStyle readStyleSpan, final boolean showToHeader)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 
getMe(boolean useObjectMe)396     static String getMe(boolean useObjectMe) {
397         return useObjectMe ? sMeObjectString : sMeSubjectString;
398     }
399 
getFormattedToHeader()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 
getSingularDraftString(Context context)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 
clearResourceCache()415     private static void clearResourceCache() {
416         sDraftSingularString = null;
417     }
418 }
419