• 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 import android.text.util.Rfc822Token;
34 import android.text.util.Rfc822Tokenizer;
35 
36 import com.android.mail.R;
37 import com.android.mail.providers.Address;
38 import com.android.mail.providers.Conversation;
39 import com.android.mail.providers.ConversationInfo;
40 import com.android.mail.providers.MessageInfo;
41 import com.android.mail.providers.UIProvider;
42 import com.android.mail.ui.DividedImageCanvas;
43 import com.android.mail.utils.ObjectCache;
44 import com.google.common.base.Objects;
45 import com.google.common.collect.Maps;
46 
47 import java.util.ArrayList;
48 import java.util.Locale;
49 import java.util.Map;
50 
51 import java.util.regex.Pattern;
52 
53 public class SendersView {
54     public static final int DEFAULT_FORMATTING = 0;
55     public static final int MERGED_FORMATTING = 1;
56     private static final Integer DOES_NOT_EXIST = -5;
57     // FIXME(ath): make all of these statics instance variables, and have callers hold onto this
58     // instance as long as appropriate (e.g. activity lifetime).
59     // no need to listen for configuration changes.
60     private static String sSendersSplitToken;
61     public static String SENDERS_VERSION_SEPARATOR = "^**^";
62     public static Pattern SENDERS_VERSION_SEPARATOR_PATTERN = Pattern.compile("\\^\\*\\*\\^");
63     private static CharSequence sDraftSingularString;
64     private static CharSequence sDraftPluralString;
65     private static CharSequence sSendingString;
66     private static String sDraftCountFormatString;
67     private static CharacterStyle sDraftsStyleSpan;
68     private static CharacterStyle sSendingStyleSpan;
69     private static TextAppearanceSpan sUnreadStyleSpan;
70     private static CharacterStyle sReadStyleSpan;
71     private static String sMeString;
72     private static Locale sMeStringLocale;
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             sMessageInfoUnreadStyleSpan = new TextAppearanceSpan(context,
124                     R.style.MessageInfoUnreadTextAppearance);
125             sMessageInfoReadStyleSpan = new TextAppearanceSpan(context,
126                     R.style.MessageInfoReadTextAppearance);
127             sDraftsStyleSpan = new TextAppearanceSpan(context, R.style.DraftTextAppearance);
128             sUnreadStyleSpan = new TextAppearanceSpan(context, R.style.SendersUnreadTextAppearance);
129             sSendingStyleSpan = new TextAppearanceSpan(context, R.style.SendingTextAppearance);
130             sReadStyleSpan = new TextAppearanceSpan(context, R.style.SendersReadTextAppearance);
131             sMessageCountSpacerString = res.getString(R.string.message_count_spacer);
132             sSendingString = res.getString(R.string.sending);
133             sBidiFormatter = BidiFormatter.getInstance();
134         }
135     }
136 
createMessageInfo(Context context, Conversation conv, final boolean resourceCachingRequired)137     public static SpannableStringBuilder createMessageInfo(Context context, Conversation conv,
138             final boolean resourceCachingRequired) {
139         SpannableStringBuilder messageInfo = new SpannableStringBuilder();
140 
141         try {
142             ConversationInfo conversationInfo = conv.conversationInfo;
143             int sendingStatus = conv.sendingState;
144             boolean hasSenders = false;
145             // This covers the case where the sender is "me" and this is a draft
146             // message, which means this will only run once most of the time.
147             for (MessageInfo m : conversationInfo.messageInfos) {
148                 if (!TextUtils.isEmpty(m.sender)) {
149                     hasSenders = true;
150                     break;
151                 }
152             }
153             getSenderResources(context, resourceCachingRequired);
154             if (conversationInfo != null) {
155                 int count = conversationInfo.messageCount;
156                 int draftCount = conversationInfo.draftCount;
157                 boolean showSending = sendingStatus == UIProvider.ConversationSendingState.SENDING;
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
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                 if (showSending) {
183                     // If we are showing a message count or any draft text, prepend
184                     // the sending state text with a comma.
185                     if (count > 1 || draftCount > 0) {
186                         messageInfo.append(sSendersSplitToken);
187                     }
188                     SpannableStringBuilder sending = new SpannableStringBuilder();
189                     sending.append(sSendingString);
190                     sending.setSpan(sSendingStyleSpan, 0, sending.length(), 0);
191                     messageInfo.append(sending);
192                 }
193                 // Prepend a space if we are showing other message info text.
194                 if (count > 1 || (draftCount > 0 && hasSenders) || showSending) {
195                     messageInfo.insert(0, sMessageCountSpacerString);
196                 }
197             }
198         } finally {
199             if (!resourceCachingRequired) {
200                 clearResourceCache();
201             }
202         }
203 
204         return messageInfo;
205     }
206 
format(Context context, ConversationInfo conversationInfo, String messageInfo, int maxChars, ArrayList<SpannableString> styledSenders, ArrayList<String> displayableSenderNames, ArrayList<String> displayableSenderEmails, String account, final boolean resourceCachingRequired)207     public static void format(Context context, ConversationInfo conversationInfo,
208             String messageInfo, int maxChars, ArrayList<SpannableString> styledSenders,
209             ArrayList<String> displayableSenderNames, ArrayList<String> displayableSenderEmails,
210             String account, final boolean resourceCachingRequired) {
211         try {
212             getSenderResources(context, resourceCachingRequired);
213             format(context, conversationInfo, messageInfo, maxChars, styledSenders,
214                     displayableSenderNames, displayableSenderEmails, account,
215                     sUnreadStyleSpan, sReadStyleSpan, resourceCachingRequired);
216         } finally {
217             if (!resourceCachingRequired) {
218                 clearResourceCache();
219             }
220         }
221     }
222 
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 resourceCachingRequired)223     public static void format(Context context, ConversationInfo conversationInfo,
224             String messageInfo, int maxChars, ArrayList<SpannableString> styledSenders,
225             ArrayList<String> displayableSenderNames, ArrayList<String> displayableSenderEmails,
226             String account, final TextAppearanceSpan notificationUnreadStyleSpan,
227             final CharacterStyle notificationReadStyleSpan, final boolean resourceCachingRequired) {
228         try {
229             getSenderResources(context, resourceCachingRequired);
230             handlePriority(context, maxChars, messageInfo, conversationInfo, styledSenders,
231                     displayableSenderNames, displayableSenderEmails, account,
232                     notificationUnreadStyleSpan, notificationReadStyleSpan);
233         } finally {
234             if (!resourceCachingRequired) {
235                 clearResourceCache();
236             }
237         }
238     }
239 
handlePriority(Context context, int maxChars, String messageInfoString, ConversationInfo conversationInfo, ArrayList<SpannableString> styledSenders, ArrayList<String> displayableSenderNames, ArrayList<String> displayableSenderEmails, String account, final TextAppearanceSpan unreadStyleSpan, final CharacterStyle readStyleSpan)240     public static void handlePriority(Context context, int maxChars, String messageInfoString,
241             ConversationInfo conversationInfo, ArrayList<SpannableString> styledSenders,
242             ArrayList<String> displayableSenderNames, ArrayList<String> displayableSenderEmails,
243             String account, final TextAppearanceSpan unreadStyleSpan,
244             final CharacterStyle readStyleSpan) {
245         boolean shouldAddPhotos = displayableSenderEmails != null;
246         int maxPriorityToInclude = -1; // inclusive
247         int numCharsUsed = messageInfoString.length(); // draft, number drafts,
248                                                        // count
249         int numSendersUsed = 0;
250         int numCharsToRemovePerWord = 0;
251         int maxFoundPriority = 0;
252         if (numCharsUsed > maxChars) {
253             numCharsToRemovePerWord = numCharsUsed - maxChars;
254         }
255 
256         final Map<Integer, Integer> priorityToLength = PRIORITY_LENGTH_MAP_CACHE.get();
257         try {
258             priorityToLength.clear();
259             int senderLength;
260             for (MessageInfo info : conversationInfo.messageInfos) {
261                 senderLength = !TextUtils.isEmpty(info.sender) ? info.sender.length() : 0;
262                 priorityToLength.put(info.priority, senderLength);
263                 maxFoundPriority = Math.max(maxFoundPriority, info.priority);
264             }
265             while (maxPriorityToInclude < maxFoundPriority) {
266                 if (priorityToLength.containsKey(maxPriorityToInclude + 1)) {
267                     int length = numCharsUsed + priorityToLength.get(maxPriorityToInclude + 1);
268                     if (numCharsUsed > 0)
269                         length += 2;
270                     // We must show at least two senders if they exist. If we don't
271                     // have space for both
272                     // then we will truncate names.
273                     if (length > maxChars && numSendersUsed >= 2) {
274                         break;
275                     }
276                     numCharsUsed = length;
277                     numSendersUsed++;
278                 }
279                 maxPriorityToInclude++;
280             }
281         } finally {
282             PRIORITY_LENGTH_MAP_CACHE.release(priorityToLength);
283         }
284         // We want to include this entry if
285         // 1) The onlyShowUnread flags is not set
286         // 2) The above flag is set, and the message is unread
287         MessageInfo currentMessage;
288         SpannableString spannableDisplay;
289         String nameString;
290         CharacterStyle style;
291         boolean appendedElided = false;
292         Map<String, Integer> displayHash = Maps.newHashMap();
293         String firstDisplayableSenderEmail = null;
294         String firstDisplayableSender = null;
295         for (int i = 0; i < conversationInfo.messageInfos.size(); i++) {
296             currentMessage = conversationInfo.messageInfos.get(i);
297             nameString = !TextUtils.isEmpty(currentMessage.sender) ? currentMessage.sender : "";
298             if (nameString.length() == 0) {
299                 nameString = getMe(context);
300             }
301             if (numCharsToRemovePerWord != 0) {
302                 nameString = nameString.substring(0,
303                         Math.max(nameString.length() - numCharsToRemovePerWord, 0));
304             }
305             final int priority = currentMessage.priority;
306             style = !currentMessage.read ? getWrappedStyleSpan(unreadStyleSpan)
307                     : getWrappedStyleSpan(readStyleSpan);
308             if (priority <= maxPriorityToInclude) {
309                 spannableDisplay = new SpannableString(sBidiFormatter.unicodeWrap(nameString));
310                 // Don't duplicate senders; leave the first instance, unless the
311                 // current instance is also unread.
312                 int oldPos = displayHash.containsKey(currentMessage.sender) ? displayHash
313                         .get(currentMessage.sender) : DOES_NOT_EXIST;
314                 // If this sender doesn't exist OR the current message is
315                 // unread, add the sender.
316                 if (oldPos == DOES_NOT_EXIST || !currentMessage.read) {
317                     // If the sender entry already existed, and is right next to the
318                     // current sender, remove the old entry.
319                     if (oldPos != DOES_NOT_EXIST && i > 0 && oldPos == i - 1
320                             && oldPos < styledSenders.size()) {
321                         // Remove the old one!
322                         styledSenders.set(oldPos, null);
323                         if (shouldAddPhotos && !TextUtils.isEmpty(currentMessage.senderEmail)) {
324                             displayableSenderEmails.remove(currentMessage.senderEmail);
325                             displayableSenderNames.remove(currentMessage.sender);
326                         }
327                     }
328                     displayHash.put(currentMessage.sender, i);
329                     spannableDisplay.setSpan(style, 0, spannableDisplay.length(), 0);
330                     styledSenders.add(spannableDisplay);
331                 }
332             } else {
333                 if (!appendedElided) {
334                     spannableDisplay = new SpannableString(sElidedString);
335                     spannableDisplay.setSpan(style, 0, spannableDisplay.length(), 0);
336                     appendedElided = true;
337                     styledSenders.add(spannableDisplay);
338                 }
339             }
340             if (shouldAddPhotos) {
341                 String senderEmail = TextUtils.isEmpty(currentMessage.sender) ?
342                         account :
343                             TextUtils.isEmpty(currentMessage.senderEmail) ?
344                                     currentMessage.sender : currentMessage.senderEmail;
345                 if (i == 0) {
346                     // Always add the first sender!
347                     firstDisplayableSenderEmail = senderEmail;
348                     firstDisplayableSender = currentMessage.sender;
349                 } else {
350                     if (!Objects.equal(firstDisplayableSenderEmail, senderEmail)) {
351                         int indexOf = displayableSenderEmails.indexOf(senderEmail);
352                         if (indexOf > -1) {
353                             displayableSenderEmails.remove(indexOf);
354                             displayableSenderNames.remove(indexOf);
355                         }
356                         displayableSenderEmails.add(senderEmail);
357                         displayableSenderNames.add(currentMessage.sender);
358                         if (displayableSenderEmails.size() > DividedImageCanvas.MAX_DIVISIONS) {
359                             displayableSenderEmails.remove(0);
360                             displayableSenderNames.remove(0);
361                         }
362                     }
363                 }
364             }
365         }
366         if (shouldAddPhotos && !TextUtils.isEmpty(firstDisplayableSenderEmail)) {
367             if (displayableSenderEmails.size() < DividedImageCanvas.MAX_DIVISIONS) {
368                 displayableSenderEmails.add(0, firstDisplayableSenderEmail);
369                 displayableSenderNames.add(0, firstDisplayableSender);
370             } else {
371                 displayableSenderEmails.set(0, firstDisplayableSenderEmail);
372                 displayableSenderNames.set(0, firstDisplayableSender);
373             }
374         }
375     }
376 
getWrappedStyleSpan(final CharacterStyle characterStyle)377     private static CharacterStyle getWrappedStyleSpan(final CharacterStyle characterStyle) {
378         return CharacterStyle.wrap(characterStyle);
379     }
380 
getMe(Context context)381     static String getMe(Context context) {
382         final Resources resources = context.getResources();
383         final Locale locale = resources.getConfiguration().locale;
384 
385         if (sMeString == null || !locale.equals(sMeStringLocale)) {
386             sMeString = resources.getString(R.string.me_subject_pronun);
387             sMeStringLocale = locale;
388         }
389         return sMeString;
390     }
391 
formatDefault(ConversationItemViewModel header, String sendersString, Context context, final CharacterStyle readStyleSpan, final boolean resourceCachingRequired)392     private static void formatDefault(ConversationItemViewModel header, String sendersString,
393             Context context, final CharacterStyle readStyleSpan,
394             final boolean resourceCachingRequired) {
395         try {
396             getSenderResources(context, resourceCachingRequired);
397             // Clear any existing sender fragments; we must re-make all of them.
398             header.senderFragments.clear();
399             // TODO: unify this with ConversationItemView.calculateTextsAndBitmaps's tokenization
400             final Rfc822Token[] senders = Rfc822Tokenizer.tokenize(sendersString);
401             final String[] namesOnly = new String[senders.length];
402             String display;
403             for (int i = 0; i < senders.length; i++) {
404                 display = Address.decodeAddressName(senders[i].getName());
405                 if (TextUtils.isEmpty(display)) {
406                     display = senders[i].getAddress();
407                 }
408                 namesOnly[i] = display;
409             }
410             generateSenderFragments(header, namesOnly, readStyleSpan);
411         } finally {
412             if (!resourceCachingRequired) {
413                 clearResourceCache();
414             }
415         }
416     }
417 
generateSenderFragments(ConversationItemViewModel header, String[] names, final CharacterStyle readStyleSpan)418     private static void generateSenderFragments(ConversationItemViewModel header, String[] names,
419             final CharacterStyle readStyleSpan) {
420         header.sendersText = TextUtils.join(Address.ADDRESS_DELIMETER + " ", names);
421         header.addSenderFragment(0, header.sendersText.length(), getWrappedStyleSpan(readStyleSpan),
422                 true);
423     }
424 
formatSenders(ConversationItemViewModel header, Context context, final boolean resourceCachingRequired)425     public static void formatSenders(ConversationItemViewModel header, Context context,
426             final boolean resourceCachingRequired) {
427         try {
428             getSenderResources(context, resourceCachingRequired);
429             formatSenders(header, context, sReadStyleSpan, resourceCachingRequired);
430         } finally {
431             if (!resourceCachingRequired) {
432                 clearResourceCache();
433             }
434         }
435     }
436 
formatSenders(ConversationItemViewModel header, Context context, final CharacterStyle readStyleSpan, final boolean resourceCachingRequired)437     public static void formatSenders(ConversationItemViewModel header, Context context,
438             final CharacterStyle readStyleSpan, final boolean resourceCachingRequired) {
439         try {
440             formatDefault(header, header.conversation.senders, context, readStyleSpan,
441                     resourceCachingRequired);
442         } finally {
443             if (!resourceCachingRequired) {
444                 clearResourceCache();
445             }
446         }
447     }
448 
clearResourceCache()449     private static void clearResourceCache() {
450         sDraftSingularString = null;
451     }
452 }
453