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