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