Home | History | Annotate | Download | only in browse
      1 /**
      2  * Copyright (c) 2011, Google Inc.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *     http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.mail.browse;
     18 
     19 import android.annotation.SuppressLint;
     20 import android.app.FragmentManager;
     21 import android.content.AsyncQueryHandler;
     22 import android.content.Context;
     23 import android.content.res.Resources;
     24 import android.database.DataSetObserver;
     25 import android.graphics.Bitmap;
     26 import android.graphics.Canvas;
     27 import android.graphics.Color;
     28 import android.graphics.Paint;
     29 import android.graphics.PorterDuff;
     30 import android.graphics.PorterDuffXfermode;
     31 import android.support.v4.text.BidiFormatter;
     32 import android.text.Html;
     33 import android.text.Spannable;
     34 import android.text.Spanned;
     35 import android.text.TextUtils;
     36 import android.text.method.LinkMovementMethod;
     37 import android.text.style.URLSpan;
     38 import android.util.AttributeSet;
     39 import android.view.LayoutInflater;
     40 import android.view.Menu;
     41 import android.view.MenuItem;
     42 import android.view.View;
     43 import android.view.View.OnClickListener;
     44 import android.view.ViewGroup;
     45 import android.widget.PopupMenu;
     46 import android.widget.PopupMenu.OnMenuItemClickListener;
     47 import android.widget.QuickContactBadge;
     48 import android.widget.TextView;
     49 import android.widget.Toast;
     50 
     51 import com.android.emailcommon.mail.Address;
     52 import com.android.mail.ContactInfo;
     53 import com.android.mail.ContactInfoSource;
     54 import com.android.mail.R;
     55 import com.android.mail.analytics.Analytics;
     56 import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem;
     57 import com.android.mail.compose.ComposeActivity;
     58 import com.android.mail.perf.Timer;
     59 import com.android.mail.photomanager.LetterTileProvider;
     60 import com.android.mail.print.PrintUtils;
     61 import com.android.mail.providers.Account;
     62 import com.android.mail.providers.Conversation;
     63 import com.android.mail.providers.Message;
     64 import com.android.mail.providers.Settings;
     65 import com.android.mail.providers.UIProvider;
     66 import com.android.mail.text.EmailAddressSpan;
     67 import com.android.mail.ui.AbstractConversationViewFragment;
     68 import com.android.mail.ui.ImageCanvas;
     69 import com.android.mail.utils.LogTag;
     70 import com.android.mail.utils.LogUtils;
     71 import com.android.mail.utils.StyleUtils;
     72 import com.android.mail.utils.Utils;
     73 import com.android.mail.utils.VeiledAddressMatcher;
     74 import com.google.common.annotations.VisibleForTesting;
     75 
     76 import java.io.IOException;
     77 import java.io.StringReader;
     78 import java.util.Map;
     79 
     80 public class MessageHeaderView extends SnapHeader implements OnClickListener,
     81         OnMenuItemClickListener, ConversationContainer.DetachListener {
     82 
     83     /**
     84      * Cap very long recipient lists during summary construction for efficiency.
     85      */
     86     private static final int SUMMARY_MAX_RECIPIENTS = 50;
     87 
     88     private static final int MAX_SNIPPET_LENGTH = 100;
     89 
     90     private static final int SHOW_IMAGE_PROMPT_ONCE = 1;
     91     private static final int SHOW_IMAGE_PROMPT_ALWAYS = 2;
     92 
     93     private static final String HEADER_RENDER_TAG = "message header render";
     94     private static final String LAYOUT_TAG = "message header layout";
     95     private static final String MEASURE_TAG = "message header measure";
     96 
     97     private static final String LOG_TAG = LogTag.getLogTag();
     98 
     99     // This is a debug only feature
    100     public static final boolean ENABLE_REPORT_RENDERING_PROBLEM = false;
    101 
    102     private MessageHeaderViewCallbacks mCallbacks;
    103 
    104     private View mBorderView;
    105     private ViewGroup mUpperHeaderView;
    106     private View mTitleContainer;
    107     private View mSnapHeaderBottomBorder;
    108     private TextView mSenderNameView;
    109     private TextView mRecipientSummary;
    110     private TextView mDateView;
    111     private View mHideDetailsView;
    112     private TextView mSnippetView;
    113     private MessageHeaderContactBadge mPhotoView;
    114     private ViewGroup mExtraContentView;
    115     private ViewGroup mExpandedDetailsView;
    116     private SpamWarningView mSpamWarningView;
    117     private TextView mImagePromptView;
    118     private MessageInviteView mInviteView;
    119     private View mForwardButton;
    120     private View mOverflowButton;
    121     private View mDraftIcon;
    122     private View mEditDraftButton;
    123     private TextView mUpperDateView;
    124     private View mReplyButton;
    125     private View mReplyAllButton;
    126     private View mAttachmentIcon;
    127     private final EmailCopyContextMenu mEmailCopyMenu;
    128 
    129     // temporary fields to reference raw data between initial render and details
    130     // expansion
    131     private String[] mFrom;
    132     private String[] mTo;
    133     private String[] mCc;
    134     private String[] mBcc;
    135     private String[] mReplyTo;
    136 
    137     private boolean mIsDraft = false;
    138 
    139     private int mSendingState;
    140 
    141     private String mSnippet;
    142 
    143     private Address mSender;
    144 
    145     private ContactInfoSource mContactInfoSource;
    146 
    147     private boolean mPreMeasuring;
    148 
    149     private ConversationAccountController mAccountController;
    150 
    151     private Map<String, Address> mAddressCache;
    152 
    153     private boolean mShowImagePrompt;
    154 
    155     private PopupMenu mPopup;
    156 
    157     private MessageHeaderItem mMessageHeaderItem;
    158     private ConversationMessage mMessage;
    159 
    160     private boolean mRecipientSummaryValid;
    161     private boolean mExpandedDetailsValid;
    162 
    163     private final LayoutInflater mInflater;
    164 
    165     private AsyncQueryHandler mQueryHandler;
    166 
    167     private boolean mObservingContactInfo;
    168 
    169     /**
    170      * What I call myself? "me" in English, and internationalized correctly.
    171      */
    172     private final String mMyName;
    173 
    174     private final DataSetObserver mContactInfoObserver = new DataSetObserver() {
    175         @Override
    176         public void onChanged() {
    177             updateContactInfo();
    178         }
    179     };
    180 
    181     private boolean mExpandable = true;
    182 
    183     private VeiledAddressMatcher mVeiledMatcher;
    184 
    185     private boolean mIsViewOnlyMode = false;
    186 
    187     private LetterTileProvider mLetterTileProvider;
    188     private final int mContactPhotoWidth;
    189     private final int mContactPhotoHeight;
    190     private final int mTitleContainerMarginEnd;
    191 
    192     /**
    193      * The snappy header has special visibility rules (i.e. no details header,
    194      * even though it has an expanded appearance)
    195      */
    196     private boolean mIsSnappy;
    197 
    198     private BidiFormatter mBidiFormatter;
    199 
    200 
    201     public interface MessageHeaderViewCallbacks {
    202         void setMessageSpacerHeight(MessageHeaderItem item, int newSpacerHeight);
    203 
    204         void setMessageExpanded(MessageHeaderItem item, int newSpacerHeight);
    205 
    206         void setMessageDetailsExpanded(MessageHeaderItem messageHeaderItem, boolean expanded,
    207                 int previousMessageHeaderItemHeight);
    208 
    209         void showExternalResources(Message msg);
    210 
    211         void showExternalResources(String senderRawAddress);
    212 
    213         boolean supportsMessageTransforms();
    214 
    215         String getMessageTransforms(Message msg);
    216 
    217         FragmentManager getFragmentManager();
    218 
    219         /**
    220          * @return <tt>true</tt> if this header is contained within a SecureConversationViewFragment
    221          * and cannot assume the content is <strong>not</strong> malicious
    222          */
    223         boolean isSecure();
    224     }
    225 
    226     public MessageHeaderView(Context context) {
    227         this(context, null);
    228     }
    229 
    230     public MessageHeaderView(Context context, AttributeSet attrs) {
    231         this(context, attrs, -1);
    232     }
    233 
    234     public MessageHeaderView(Context context, AttributeSet attrs, int defStyle) {
    235         super(context, attrs, defStyle);
    236 
    237         mIsSnappy = false;
    238         mEmailCopyMenu = new EmailCopyContextMenu(getContext());
    239         mInflater = LayoutInflater.from(context);
    240         mMyName = context.getString(R.string.me_object_pronoun);
    241 
    242         final Resources res = getResources();
    243         mContactPhotoWidth = res.getDimensionPixelSize(R.dimen.contact_image_width);
    244         mContactPhotoHeight = res.getDimensionPixelSize(R.dimen.contact_image_height);
    245         mTitleContainerMarginEnd = res.getDimensionPixelSize(R.dimen.conversation_view_margin_side);
    246     }
    247 
    248     @Override
    249     protected void onFinishInflate() {
    250         super.onFinishInflate();
    251         mBorderView = findViewById(R.id.message_header_border);
    252         mUpperHeaderView = (ViewGroup) findViewById(R.id.upper_header);
    253         mTitleContainer = findViewById(R.id.title_container);
    254         mSnapHeaderBottomBorder = findViewById(R.id.snap_header_bottom_border);
    255         mSenderNameView = (TextView) findViewById(R.id.sender_name);
    256         mRecipientSummary = (TextView) findViewById(R.id.recipient_summary);
    257         mDateView = (TextView) findViewById(R.id.send_date);
    258         mHideDetailsView = findViewById(R.id.hide_details);
    259         mSnippetView = (TextView) findViewById(R.id.email_snippet);
    260         mPhotoView = (MessageHeaderContactBadge) findViewById(R.id.photo);
    261         mPhotoView.setQuickContactBadge(
    262                 (QuickContactBadge) findViewById(R.id.invisible_quick_contact));
    263         mReplyButton = findViewById(R.id.reply);
    264         mReplyAllButton = findViewById(R.id.reply_all);
    265         mForwardButton = findViewById(R.id.forward);
    266         mOverflowButton = findViewById(R.id.overflow);
    267         mDraftIcon = findViewById(R.id.draft);
    268         mEditDraftButton = findViewById(R.id.edit_draft);
    269         mUpperDateView = (TextView) findViewById(R.id.upper_date);
    270         mAttachmentIcon = findViewById(R.id.attachment);
    271         mExtraContentView = (ViewGroup) findViewById(R.id.header_extra_content);
    272 
    273         setExpanded(true);
    274 
    275         registerMessageClickTargets(mReplyButton, mReplyAllButton, mForwardButton,
    276                 mEditDraftButton, mOverflowButton, mUpperHeaderView, mDateView, mHideDetailsView);
    277 
    278         mUpperHeaderView.setOnCreateContextMenuListener(mEmailCopyMenu);
    279     }
    280 
    281     private void registerMessageClickTargets(View... views) {
    282         for (View v : views) {
    283             if (v != null) {
    284                 v.setOnClickListener(this);
    285             }
    286         }
    287     }
    288 
    289     @Override
    290     public void initialize(ConversationAccountController accountController,
    291             Map<String, Address> addressCache, MessageHeaderViewCallbacks callbacks,
    292             ContactInfoSource contactInfoSource, VeiledAddressMatcher veiledAddressMatcher) {
    293         initialize(accountController, addressCache);
    294         setCallbacks(callbacks);
    295         setContactInfoSource(contactInfoSource);
    296         setVeiledMatcher(veiledAddressMatcher);
    297     }
    298 
    299     /**
    300      * Associate the header with a contact info source for later contact
    301      * presence/photo lookup.
    302      */
    303     public void setContactInfoSource(ContactInfoSource contactInfoSource) {
    304         mContactInfoSource = contactInfoSource;
    305     }
    306 
    307     public void setCallbacks(MessageHeaderViewCallbacks callbacks) {
    308         mCallbacks = callbacks;
    309     }
    310 
    311     public void setVeiledMatcher(VeiledAddressMatcher matcher) {
    312         mVeiledMatcher = matcher;
    313     }
    314 
    315     public boolean isExpanded() {
    316         // (let's just arbitrarily say that unbound views are expanded by default)
    317         return mMessageHeaderItem == null || mMessageHeaderItem.isExpanded();
    318     }
    319 
    320     @Override
    321     public void onDetachedFromParent() {
    322         unbind();
    323     }
    324 
    325     /**
    326      * Headers that are unbound will not match any rendered header (matches()
    327      * will return false). Unbinding is not guaranteed to *hide* the view's old
    328      * data, though. To re-bind this header to message data, call render() or
    329      * renderUpperHeaderFrom().
    330      */
    331     @Override
    332     public void unbind() {
    333         mMessageHeaderItem = null;
    334         mMessage = null;
    335 
    336         if (mObservingContactInfo) {
    337             mContactInfoSource.unregisterObserver(mContactInfoObserver);
    338             mObservingContactInfo = false;
    339         }
    340     }
    341 
    342     public void initialize(ConversationAccountController accountController,
    343             Map<String, Address> addressCache) {
    344         mAccountController = accountController;
    345         mAddressCache = addressCache;
    346     }
    347 
    348     private Account getAccount() {
    349         return mAccountController != null ? mAccountController.getAccount() : null;
    350     }
    351 
    352     public void bind(MessageHeaderItem headerItem, boolean measureOnly) {
    353         if (mMessageHeaderItem != null && mMessageHeaderItem == headerItem) {
    354             return;
    355         }
    356 
    357         mMessageHeaderItem = headerItem;
    358         render(measureOnly);
    359     }
    360 
    361     /**
    362      * Rebinds the view to its data. This will only update the view
    363      * if the {@link MessageHeaderItem} sent as a parameter is the
    364      * same as the view's current {@link MessageHeaderItem} and the
    365      * view's expanded state differs from the item's expanded state.
    366      */
    367     public void rebind(MessageHeaderItem headerItem) {
    368         if (mMessageHeaderItem == null || mMessageHeaderItem != headerItem ||
    369                 isActivated() == isExpanded()) {
    370             return;
    371         }
    372 
    373         render(false /* measureOnly */);
    374     }
    375 
    376     @Override
    377     public void refresh() {
    378         render(false);
    379     }
    380 
    381     private BidiFormatter getBidiFormatter() {
    382         if (mBidiFormatter == null) {
    383             final ConversationViewAdapter adapter = mMessageHeaderItem != null
    384                     ? mMessageHeaderItem.getAdapter() : null;
    385             if (adapter == null) {
    386                 mBidiFormatter = BidiFormatter.getInstance();
    387             } else {
    388                 mBidiFormatter = adapter.getBidiFormatter();
    389             }
    390         }
    391         return mBidiFormatter;
    392     }
    393 
    394     private void render(boolean measureOnly) {
    395         if (mMessageHeaderItem == null) {
    396             return;
    397         }
    398 
    399         Timer t = new Timer();
    400         t.start(HEADER_RENDER_TAG);
    401 
    402         mRecipientSummaryValid = false;
    403         mExpandedDetailsValid = false;
    404 
    405         mMessage = mMessageHeaderItem.getMessage();
    406 
    407         final Account account = getAccount();
    408         final boolean alwaysShowImagesForAccount = (account != null) &&
    409                 (account.settings.showImages == Settings.ShowImages.ALWAYS);
    410 
    411         final boolean alwaysShowImagesForMessage = mMessage.shouldShowImagePrompt();
    412 
    413         if (!alwaysShowImagesForMessage) {
    414             // we don't need the "Show picture" prompt if the user allows images for this message
    415             mShowImagePrompt = false;
    416         } else if (mCallbacks.isSecure()) {
    417             // in a secure view we always display the "Show picture" prompt
    418             mShowImagePrompt = true;
    419         } else {
    420             // otherwise honor the account setting for automatically showing pictures
    421             mShowImagePrompt = !alwaysShowImagesForAccount;
    422         }
    423 
    424         setExpanded(mMessageHeaderItem.isExpanded());
    425 
    426         mFrom = mMessage.getFromAddresses();
    427         mTo = mMessage.getToAddresses();
    428         mCc = mMessage.getCcAddresses();
    429         mBcc = mMessage.getBccAddresses();
    430         mReplyTo = mMessage.getReplyToAddresses();
    431 
    432         /**
    433          * Turns draft mode on or off. Draft mode hides message operations other
    434          * than "edit", hides contact photo, hides presence, and changes the
    435          * sender name to "Draft".
    436          */
    437         mIsDraft = mMessage.draftType != UIProvider.DraftType.NOT_A_DRAFT;
    438         mSendingState = mMessage.sendingState;
    439 
    440         // If this was a sent message AND:
    441         // 1. the account has a custom from, the cursor will populate the
    442         // selected custom from as the fromAddress when a message is sent but
    443         // not yet synced.
    444         // 2. the account has no custom froms, fromAddress will be empty, and we
    445         // can safely fall back and show the account name as sender since it's
    446         // the only possible fromAddress.
    447         String from = mMessage.getFrom();
    448         if (TextUtils.isEmpty(from)) {
    449             from = (account != null) ? account.getEmailAddress() : "";
    450         }
    451         mSender = getAddress(from);
    452 
    453         updateChildVisibility();
    454 
    455         final String snippet;
    456         if (mIsDraft || mSendingState != UIProvider.ConversationSendingState.OTHER) {
    457             snippet = makeSnippet(mMessage.snippet);
    458         } else {
    459             snippet = mMessage.snippet;
    460         }
    461         mSnippet = snippet == null ? null : getBidiFormatter().unicodeWrap(snippet);
    462 
    463         mSenderNameView.setText(getHeaderTitle());
    464         setRecipientSummary();
    465         setDateText();
    466         mSnippetView.setText(mSnippet);
    467         setAddressOnContextMenu();
    468 
    469         if (mUpperDateView != null) {
    470             mUpperDateView.setText(mMessageHeaderItem.getTimestampShort());
    471         }
    472 
    473         if (measureOnly) {
    474             // avoid leaving any state around that would interfere with future regular bind() calls
    475             unbind();
    476         } else {
    477             updateContactInfo();
    478             if (!mObservingContactInfo) {
    479                 mContactInfoSource.registerObserver(mContactInfoObserver);
    480                 mObservingContactInfo = true;
    481             }
    482         }
    483 
    484         t.pause(HEADER_RENDER_TAG);
    485     }
    486 
    487     /**
    488      * Update context menu's address field for when the user long presses
    489      * on the message header and attempts to copy/send email.
    490      */
    491     private void setAddressOnContextMenu() {
    492         if (mSender != null) {
    493             mEmailCopyMenu.setAddress(mSender.getAddress());
    494         }
    495     }
    496 
    497     @Override
    498     public boolean isBoundTo(ConversationOverlayItem item) {
    499         return item == mMessageHeaderItem;
    500     }
    501 
    502     public Address getAddress(String emailStr) {
    503         return Utils.getAddress(mAddressCache, emailStr);
    504     }
    505 
    506     private void updateSpacerHeight() {
    507         final int h = measureHeight();
    508 
    509         mMessageHeaderItem.setHeight(h);
    510         if (mCallbacks != null) {
    511             mCallbacks.setMessageSpacerHeight(mMessageHeaderItem, h);
    512         }
    513     }
    514 
    515     private int measureHeight() {
    516         ViewGroup parent = (ViewGroup) getParent();
    517         if (parent == null) {
    518             LogUtils.e(LOG_TAG, new Error(), "Unable to measure height of detached header");
    519             return getHeight();
    520         }
    521         mPreMeasuring = true;
    522         final int h = Utils.measureViewHeight(this, parent);
    523         mPreMeasuring = false;
    524         return h;
    525     }
    526 
    527     private CharSequence getHeaderTitle() {
    528         CharSequence title;
    529         switch (mSendingState) {
    530             case UIProvider.ConversationSendingState.QUEUED:
    531             case UIProvider.ConversationSendingState.SENDING:
    532                 title = getResources().getString(R.string.sending);
    533                 break;
    534             case UIProvider.ConversationSendingState.RETRYING:
    535                 title = getResources().getString(R.string.message_retrying);
    536                 break;
    537             case UIProvider.ConversationSendingState.SEND_ERROR:
    538                 title = getResources().getString(R.string.message_failed);
    539                 break;
    540             default:
    541                 if (mIsDraft) {
    542                     title = SendersView.getSingularDraftString(getContext());
    543                 } else {
    544                     title = getBidiFormatter().unicodeWrap(
    545                             getSenderName(mSender));
    546                 }
    547         }
    548 
    549         return title;
    550     }
    551 
    552     private void setRecipientSummary() {
    553         if (!mRecipientSummaryValid) {
    554             if (mMessageHeaderItem.recipientSummaryText == null) {
    555                 final Account account = getAccount();
    556                 final String meEmailAddress = (account != null) ? account.getEmailAddress() : "";
    557                 mMessageHeaderItem.recipientSummaryText = getRecipientSummaryText(getContext(),
    558                         meEmailAddress, mMyName, mTo, mCc, mBcc, mAddressCache, mVeiledMatcher,
    559                         getBidiFormatter());
    560             }
    561             mRecipientSummary.setText(mMessageHeaderItem.recipientSummaryText);
    562             mRecipientSummaryValid = true;
    563         }
    564     }
    565 
    566     private void setDateText() {
    567         if (mIsSnappy) {
    568             mDateView.setText(mMessageHeaderItem.getTimestampLong());
    569             mDateView.setOnClickListener(null);
    570         } else {
    571             mDateView.setMovementMethod(LinkMovementMethod.getInstance());
    572             mDateView.setText(Html.fromHtml(getResources().getString(
    573                     R.string.date_and_view_details, mMessageHeaderItem.getTimestampLong())));
    574             StyleUtils.stripUnderlinesAndUrl(mDateView);
    575         }
    576     }
    577 
    578     /**
    579      * Return the name, if known, or just the address.
    580      */
    581     private static String getSenderName(Address sender) {
    582         if (sender == null) {
    583             return "";
    584         }
    585         final String displayName = sender.getPersonal();
    586         return TextUtils.isEmpty(displayName) ? sender.getAddress() : displayName;
    587     }
    588 
    589     private static void setChildVisibility(int visibility, View... children) {
    590         for (View v : children) {
    591             if (v != null) {
    592                 v.setVisibility(visibility);
    593             }
    594         }
    595     }
    596 
    597     private void setExpanded(final boolean expanded) {
    598         // use View's 'activated' flag to store expanded state
    599         // child view state lists can use this to toggle drawables
    600         setActivated(expanded);
    601         if (mMessageHeaderItem != null) {
    602             mMessageHeaderItem.setExpanded(expanded);
    603         }
    604     }
    605 
    606     /**
    607      * Update the visibility of the many child views based on expanded/collapsed
    608      * and draft/normal state.
    609      */
    610     private void updateChildVisibility() {
    611         // Too bad this can't be done with an XML state list...
    612 
    613         if (mIsViewOnlyMode) {
    614             setMessageDetailsVisibility(VISIBLE);
    615             setChildVisibility(GONE, mSnapHeaderBottomBorder);
    616 
    617             setChildVisibility(GONE, mReplyButton, mReplyAllButton, mForwardButton,
    618                     mOverflowButton, mDraftIcon, mEditDraftButton,
    619                     mAttachmentIcon, mUpperDateView, mSnippetView);
    620             setChildVisibility(VISIBLE, mPhotoView, mRecipientSummary);
    621 
    622             setChildMarginEnd(mTitleContainer, 0);
    623         } else if (isExpanded()) {
    624             int normalVis, draftVis;
    625 
    626             final boolean isSnappy = isSnappy();
    627             setMessageDetailsVisibility((isSnappy) ? GONE : VISIBLE);
    628             setChildVisibility(isSnappy ? VISIBLE : GONE, mSnapHeaderBottomBorder);
    629 
    630             if (mIsDraft) {
    631                 normalVis = GONE;
    632                 draftVis = VISIBLE;
    633             } else {
    634                 normalVis = VISIBLE;
    635                 draftVis = GONE;
    636             }
    637 
    638             setReplyOrReplyAllVisible();
    639             setChildVisibility(normalVis, mPhotoView, mForwardButton, mOverflowButton);
    640             setChildVisibility(draftVis, mDraftIcon, mEditDraftButton);
    641             setChildVisibility(VISIBLE, mRecipientSummary);
    642             setChildVisibility(GONE, mAttachmentIcon, mUpperDateView, mSnippetView);
    643 
    644             setChildMarginEnd(mTitleContainer, 0);
    645         } else {
    646             setMessageDetailsVisibility(GONE);
    647             setChildVisibility(GONE, mSnapHeaderBottomBorder);
    648             setChildVisibility(VISIBLE, mSnippetView, mUpperDateView);
    649 
    650             setChildVisibility(GONE, mEditDraftButton, mReplyButton, mReplyAllButton,
    651                     mForwardButton, mOverflowButton, mRecipientSummary,
    652                     mDateView, mHideDetailsView);
    653 
    654             setChildVisibility(mMessage.hasAttachments ? VISIBLE : GONE,
    655                     mAttachmentIcon);
    656 
    657             if (mIsDraft) {
    658                 setChildVisibility(VISIBLE, mDraftIcon);
    659                 setChildVisibility(GONE, mPhotoView);
    660             } else {
    661                 setChildVisibility(GONE, mDraftIcon);
    662                 setChildVisibility(VISIBLE, mPhotoView);
    663             }
    664 
    665             setChildMarginEnd(mTitleContainer, mTitleContainerMarginEnd);
    666         }
    667 
    668         final ConversationViewAdapter adapter = mMessageHeaderItem.getAdapter();
    669         if (adapter != null) {
    670             mBorderView.setVisibility(
    671                     adapter.isPreviousItemSuperCollapsed(mMessageHeaderItem) ? GONE : VISIBLE);
    672         } else {
    673             mBorderView.setVisibility(VISIBLE);
    674         }
    675     }
    676 
    677     /**
    678      * If an overflow menu is present in this header's layout, set the
    679      * visibility of "Reply" and "Reply All" actions based on a user preference.
    680      * Only one of those actions will be visible when an overflow is present. If
    681      * no overflow is present (e.g. big phone or tablet), it's assumed we have
    682      * plenty of screen real estate and can show both.
    683      */
    684     private void setReplyOrReplyAllVisible() {
    685         if (mIsDraft) {
    686             setChildVisibility(GONE, mReplyButton, mReplyAllButton);
    687             return;
    688         } else if (mOverflowButton == null) {
    689             setChildVisibility(VISIBLE, mReplyButton, mReplyAllButton);
    690             return;
    691         }
    692 
    693         final Account account = getAccount();
    694         final boolean defaultReplyAll = (account != null) ? account.settings.replyBehavior
    695                 == UIProvider.DefaultReplyBehavior.REPLY_ALL : false;
    696         setChildVisibility(defaultReplyAll ? GONE : VISIBLE, mReplyButton);
    697         setChildVisibility(defaultReplyAll ? VISIBLE : GONE, mReplyAllButton);
    698     }
    699 
    700     @SuppressLint("NewApi")
    701     private static void setChildMarginEnd(View childView, int marginEnd) {
    702         MarginLayoutParams mlp = (MarginLayoutParams) childView.getLayoutParams();
    703         if (Utils.isRunningJBMR1OrLater()) {
    704             mlp.setMarginEnd(marginEnd);
    705         } else {
    706             mlp.rightMargin = marginEnd;
    707         }
    708         childView.setLayoutParams(mlp);
    709     }
    710 
    711 
    712 
    713     @VisibleForTesting
    714     static CharSequence getRecipientSummaryText(Context context, String meEmailAddress,
    715             String myName, String[] to, String[] cc, String[] bcc,
    716             Map<String, Address> addressCache, VeiledAddressMatcher matcher,
    717             BidiFormatter bidiFormatter) {
    718 
    719         final RecipientListsBuilder builder = new RecipientListsBuilder(
    720                 context, meEmailAddress, myName, addressCache, matcher, bidiFormatter);
    721 
    722         builder.append(to);
    723         builder.append(cc);
    724         builder.append(bcc);
    725 
    726         return builder.build();
    727     }
    728 
    729     /**
    730      * Utility class to build a list of recipient lists.
    731      */
    732     private static class RecipientListsBuilder {
    733         private final Context mContext;
    734         private final String mMeEmailAddress;
    735         private final String mMyName;
    736         private final StringBuilder mBuilder = new StringBuilder();
    737         private final CharSequence mComma;
    738         private final Map<String, Address> mAddressCache;
    739         private final VeiledAddressMatcher mMatcher;
    740         private final BidiFormatter mBidiFormatter;
    741 
    742         int mRecipientCount = 0;
    743         boolean mFirst = true;
    744 
    745         public RecipientListsBuilder(Context context, String meEmailAddress, String myName,
    746                 Map<String, Address> addressCache, VeiledAddressMatcher matcher,
    747                 BidiFormatter bidiFormatter) {
    748             mContext = context;
    749             mMeEmailAddress = meEmailAddress;
    750             mMyName = myName;
    751             mComma = mContext.getText(R.string.enumeration_comma);
    752             mAddressCache = addressCache;
    753             mMatcher = matcher;
    754             mBidiFormatter = bidiFormatter;
    755         }
    756 
    757         public void append(String[] recipients) {
    758             final int addLimit = SUMMARY_MAX_RECIPIENTS - mRecipientCount;
    759             final boolean hasRecipients = appendRecipients(recipients, addLimit);
    760             if (hasRecipients) {
    761                 mRecipientCount += Math.min(addLimit, recipients.length);
    762             }
    763         }
    764 
    765         /**
    766          * Appends formatted recipients of the message to the recipient list,
    767          * as long as there are recipients left to append and the maximum number
    768          * of addresses limit has not been reached.
    769          * @param rawAddrs The addresses to append.
    770          * @param maxToCopy The maximum number of addresses to append.
    771          * @return {@code true} if a recipient has been appended. {@code false}, otherwise.
    772          */
    773         private boolean appendRecipients(String[] rawAddrs,
    774                 int maxToCopy) {
    775             if (rawAddrs == null || rawAddrs.length == 0 || maxToCopy == 0) {
    776                 return false;
    777             }
    778 
    779             final int len = Math.min(maxToCopy, rawAddrs.length);
    780             for (int i = 0; i < len; i++) {
    781                 final Address email = Utils.getAddress(mAddressCache, rawAddrs[i]);
    782                 final String emailAddress = email.getAddress();
    783                 final String name;
    784                 if (mMatcher != null && mMatcher.isVeiledAddress(emailAddress)) {
    785                     if (TextUtils.isEmpty(email.getPersonal())) {
    786                         // Let's write something more readable.
    787                         name = mContext.getString(VeiledAddressMatcher.VEILED_SUMMARY_UNKNOWN);
    788                     } else {
    789                         name = email.getSimplifiedName();
    790                     }
    791                 } else {
    792                     // Not a veiled address, show first part of email, or "me".
    793                     name = mMeEmailAddress.equals(emailAddress) ?
    794                             mMyName : email.getSimplifiedName();
    795                 }
    796 
    797                 // duplicate TextUtils.join() logic to minimize temporary allocations
    798                 if (mFirst) {
    799                     mFirst = false;
    800                 } else {
    801                     mBuilder.append(mComma);
    802                 }
    803                 mBuilder.append(mBidiFormatter.unicodeWrap(name));
    804             }
    805 
    806             return true;
    807         }
    808 
    809         public CharSequence build() {
    810             return mContext.getString(R.string.to_message_header, mBuilder);
    811         }
    812     }
    813 
    814     private void updateContactInfo() {
    815         if (mContactInfoSource == null || mSender == null) {
    816             mPhotoView.setImageToDefault();
    817             mPhotoView.setContentDescription(getResources().getString(
    818                     R.string.contact_info_string_default));
    819             return;
    820         }
    821 
    822         // Set the photo to either a found Bitmap or the default
    823         // and ensure either the contact URI or email is set so the click
    824         // handling works
    825         String contentDesc = getResources().getString(R.string.contact_info_string,
    826                 !TextUtils.isEmpty(mSender.getPersonal())
    827                         ? mSender.getPersonal()
    828                         : mSender.getAddress());
    829         mPhotoView.setContentDescription(contentDesc);
    830         boolean photoSet = false;
    831         final String email = mSender.getAddress();
    832         final ContactInfo info = mContactInfoSource.getContactInfo(email);
    833         final Resources res = getResources();
    834         if (info != null) {
    835             if (info.contactUri != null) {
    836                 mPhotoView.assignContactUri(info.contactUri);
    837             } else {
    838                 mPhotoView.assignContactFromEmail(email, true /* lazyLookup */);
    839             }
    840 
    841             if (info.photo != null) {
    842                 mPhotoView.setImageBitmap(frameBitmapInCircle(info.photo));
    843                 photoSet = true;
    844             }
    845         } else {
    846             mPhotoView.assignContactFromEmail(email, true /* lazyLookup */);
    847         }
    848 
    849         if (!photoSet) {
    850             mPhotoView.setImageBitmap(
    851                     frameBitmapInCircle(makeLetterTile(mSender.getPersonal(), email)));
    852         }
    853     }
    854 
    855     private Bitmap makeLetterTile(
    856             String displayName, String senderAddress) {
    857         if (mLetterTileProvider == null) {
    858             mLetterTileProvider = new LetterTileProvider(getContext());
    859         }
    860 
    861         final ImageCanvas.Dimensions dimensions = new ImageCanvas.Dimensions(
    862                 mContactPhotoWidth, mContactPhotoHeight, ImageCanvas.Dimensions.SCALE_ONE);
    863         return mLetterTileProvider.getLetterTile(dimensions, displayName, senderAddress);
    864     }
    865 
    866     /**
    867      * Frames the input bitmap in a circle.
    868      */
    869     private static Bitmap frameBitmapInCircle(Bitmap input) {
    870         if (input == null) {
    871             return null;
    872         }
    873 
    874         // Crop the image if not squared.
    875         int inputWidth = input.getWidth();
    876         int inputHeight = input.getHeight();
    877         int targetX, targetY, targetSize;
    878         if (inputWidth >= inputHeight) {
    879             targetX = inputWidth / 2 - inputHeight / 2;
    880             targetY = 0;
    881             targetSize = inputHeight;
    882         } else {
    883             targetX = 0;
    884             targetY = inputHeight / 2 - inputWidth / 2;
    885             targetSize = inputWidth;
    886         }
    887 
    888         // Create an output bitmap and a canvas to draw on it.
    889         Bitmap output = Bitmap.createBitmap(targetSize, targetSize, Bitmap.Config.ARGB_8888);
    890         Canvas canvas = new Canvas(output);
    891 
    892         // Create a black paint to draw the mask.
    893         Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    894         paint.setColor(Color.BLACK);
    895 
    896         // Draw a circle.
    897         canvas.drawCircle(targetSize / 2, targetSize / 2, targetSize / 2, paint);
    898 
    899         // Replace the black parts of the mask with the input image.
    900         paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
    901         canvas.drawBitmap(input, targetX /* left */, targetY /* top */, paint);
    902 
    903         return output;
    904     }
    905 
    906     @Override
    907     public boolean onMenuItemClick(MenuItem item) {
    908         mPopup.dismiss();
    909         return onClick(null, item.getItemId());
    910     }
    911 
    912     @Override
    913     public void onClick(View v) {
    914         onClick(v, v.getId());
    915     }
    916 
    917     /**
    918      * Handles clicks on either views or menu items. View parameter can be null
    919      * for menu item clicks.
    920      */
    921     public boolean onClick(final View v, final int id) {
    922         if (mMessage == null) {
    923             LogUtils.i(LOG_TAG, "ignoring message header tap on unbound view");
    924             return false;
    925         }
    926 
    927         boolean handled = true;
    928 
    929         if (id == R.id.reply) {
    930             ComposeActivity.reply(getContext(), getAccount(), mMessage);
    931         } else if (id == R.id.reply_all) {
    932             ComposeActivity.replyAll(getContext(), getAccount(), mMessage);
    933         } else if (id == R.id.forward) {
    934             ComposeActivity.forward(getContext(), getAccount(), mMessage);
    935         } else if (id == R.id.print_message) {
    936             printMessage();
    937         } else if (id == R.id.report_rendering_problem) {
    938             final String text = getContext().getString(R.string.report_rendering_problem_desc);
    939             ComposeActivity.reportRenderingFeedback(getContext(), getAccount(), mMessage,
    940                     text + "\n\n" + mCallbacks.getMessageTransforms(mMessage));
    941         } else if (id == R.id.report_rendering_improvement) {
    942             final String text = getContext().getString(R.string.report_rendering_improvement_desc);
    943             ComposeActivity.reportRenderingFeedback(getContext(), getAccount(), mMessage,
    944                     text + "\n\n" + mCallbacks.getMessageTransforms(mMessage));
    945         } else if (id == R.id.edit_draft) {
    946             ComposeActivity.editDraft(getContext(), getAccount(), mMessage);
    947         } else if (id == R.id.overflow) {
    948             if (mPopup == null) {
    949                 mPopup = new PopupMenu(getContext(), v);
    950                 mPopup.getMenuInflater().inflate(R.menu.message_header_overflow_menu,
    951                         mPopup.getMenu());
    952                 mPopup.setOnMenuItemClickListener(this);
    953             }
    954             final boolean defaultReplyAll = getAccount().settings.replyBehavior
    955                     == UIProvider.DefaultReplyBehavior.REPLY_ALL;
    956             final Menu m = mPopup.getMenu();
    957             m.findItem(R.id.reply).setVisible(defaultReplyAll);
    958             m.findItem(R.id.reply_all).setVisible(!defaultReplyAll);
    959             m.findItem(R.id.print_message).setVisible(Utils.isRunningKitkatOrLater());
    960 
    961             final boolean reportRendering = ENABLE_REPORT_RENDERING_PROBLEM
    962                 && mCallbacks.supportsMessageTransforms();
    963             m.findItem(R.id.report_rendering_improvement).setVisible(reportRendering);
    964             m.findItem(R.id.report_rendering_problem).setVisible(reportRendering);
    965 
    966             mPopup.show();
    967         } else if (id == R.id.send_date || id == R.id.hide_details ||
    968                 id == R.id.details_expanded_content) {
    969             toggleMessageDetails();
    970         } else if (id == R.id.upper_header) {
    971             toggleExpanded();
    972         } else if (id == R.id.show_pictures_text) {
    973             handleShowImagePromptClick(v);
    974         } else {
    975             LogUtils.i(LOG_TAG, "unrecognized header tap: %d", id);
    976             handled = false;
    977         }
    978 
    979         if (handled && id != R.id.overflow) {
    980             Analytics.getInstance().sendMenuItemEvent(Analytics.EVENT_CATEGORY_MENU_ITEM, id,
    981                     "message_header", 0);
    982         }
    983 
    984         return handled;
    985     }
    986 
    987     private void printMessage() {
    988         // Secure conversation view does not use a conversation view adapter
    989         // so it's safe to test for existence as a signal to use javascript or not.
    990         final boolean useJavascript = mMessageHeaderItem.getAdapter() != null;
    991         final Account account = getAccount();
    992         final Conversation conversation = mMessage.getConversation();
    993         final String baseUri =
    994                 AbstractConversationViewFragment.buildBaseUri(getContext(), account, conversation);
    995         PrintUtils.printMessage(getContext(), mMessage, conversation.subject,
    996                 mAddressCache, conversation.getBaseUri(baseUri), useJavascript);
    997     }
    998 
    999     /**
   1000      * Set to true if the user should not be able to perform message actions
   1001      * on the message such as reply/reply all/forward/star/etc.
   1002      *
   1003      * Default is false.
   1004      */
   1005     public void setViewOnlyMode(boolean isViewOnlyMode) {
   1006         mIsViewOnlyMode = isViewOnlyMode;
   1007     }
   1008 
   1009     public void setExpandable(boolean expandable) {
   1010         mExpandable = expandable;
   1011     }
   1012 
   1013     public void toggleExpanded() {
   1014         if (!mExpandable) {
   1015             return;
   1016         }
   1017         setExpanded(!isExpanded());
   1018 
   1019         // The snappy header will disappear; no reason to update text.
   1020         if (!isSnappy()) {
   1021             mSenderNameView.setText(getHeaderTitle());
   1022             setRecipientSummary();
   1023             setDateText();
   1024             mSnippetView.setText(mSnippet);
   1025         }
   1026 
   1027         updateChildVisibility();
   1028 
   1029         // Force-measure the new header height so we can set the spacer size and
   1030         // reveal the message div in one pass. Force-measuring makes it unnecessary to set
   1031         // mSizeChanged.
   1032         int h = measureHeight();
   1033         mMessageHeaderItem.setHeight(h);
   1034         if (mCallbacks != null) {
   1035             mCallbacks.setMessageExpanded(mMessageHeaderItem, h);
   1036         }
   1037     }
   1038 
   1039     private static boolean isValidPosition(int position, int size) {
   1040         return position >= 0 && position < size;
   1041     }
   1042 
   1043     @Override
   1044     public void setSnappy() {
   1045         mIsSnappy = true;
   1046         hideMessageDetails();
   1047     }
   1048 
   1049     private boolean isSnappy() {
   1050         return mIsSnappy;
   1051     }
   1052 
   1053     private void toggleMessageDetails() {
   1054         int heightBefore = measureHeight();
   1055         final boolean expand =
   1056                 (mExpandedDetailsView == null || mExpandedDetailsView.getVisibility() == GONE);
   1057         setMessageDetailsExpanded(expand);
   1058         updateSpacerHeight();
   1059         if (mCallbacks != null) {
   1060             mCallbacks.setMessageDetailsExpanded(mMessageHeaderItem, expand, heightBefore);
   1061         }
   1062     }
   1063 
   1064     private void setMessageDetailsExpanded(boolean expand) {
   1065         if (expand) {
   1066             showExpandedDetails();
   1067         } else {
   1068             hideExpandedDetails();
   1069         }
   1070 
   1071         if (mMessageHeaderItem != null) {
   1072             mMessageHeaderItem.detailsExpanded = expand;
   1073         }
   1074     }
   1075 
   1076     public void setMessageDetailsVisibility(int vis) {
   1077         if (vis == GONE) {
   1078             hideExpandedDetails();
   1079             hideSpamWarning();
   1080             hideShowImagePrompt();
   1081             hideInvite();
   1082             mUpperHeaderView.setOnCreateContextMenuListener(null);
   1083         } else {
   1084             setMessageDetailsExpanded(mMessageHeaderItem.detailsExpanded);
   1085             if (mMessage.spamWarningString == null) {
   1086                 hideSpamWarning();
   1087             } else {
   1088                 showSpamWarning();
   1089             }
   1090             if (mShowImagePrompt) {
   1091                 if (mMessageHeaderItem.getShowImages()) {
   1092                     showImagePromptAlways(true);
   1093                 } else {
   1094                     showImagePromptOnce();
   1095                 }
   1096             } else {
   1097                 hideShowImagePrompt();
   1098             }
   1099             if (mMessage.isFlaggedCalendarInvite()) {
   1100                 showInvite();
   1101             } else {
   1102                 hideInvite();
   1103             }
   1104             mUpperHeaderView.setOnCreateContextMenuListener(mEmailCopyMenu);
   1105         }
   1106     }
   1107 
   1108     private void hideMessageDetails() {
   1109         setMessageDetailsVisibility(GONE);
   1110     }
   1111 
   1112     private void hideExpandedDetails() {
   1113         if (mExpandedDetailsView != null) {
   1114             mExpandedDetailsView.setVisibility(GONE);
   1115         }
   1116         mDateView.setVisibility(VISIBLE);
   1117         mHideDetailsView.setVisibility(GONE);
   1118     }
   1119 
   1120     private void hideInvite() {
   1121         if (mInviteView != null) {
   1122             mInviteView.setVisibility(GONE);
   1123         }
   1124     }
   1125 
   1126     private void showInvite() {
   1127         if (mInviteView == null) {
   1128             mInviteView = (MessageInviteView) mInflater.inflate(
   1129                     R.layout.conversation_message_invite, this, false);
   1130             mExtraContentView.addView(mInviteView);
   1131         }
   1132         mInviteView.bind(mMessage);
   1133         mInviteView.setVisibility(VISIBLE);
   1134     }
   1135 
   1136     private void hideShowImagePrompt() {
   1137         if (mImagePromptView != null) {
   1138             mImagePromptView.setVisibility(GONE);
   1139         }
   1140     }
   1141 
   1142     private void showImagePromptOnce() {
   1143         if (mImagePromptView == null) {
   1144             mImagePromptView = (TextView) mInflater.inflate(
   1145                     R.layout.conversation_message_show_pics, this, false);
   1146             mExtraContentView.addView(mImagePromptView);
   1147             mImagePromptView.setOnClickListener(this);
   1148         }
   1149         mImagePromptView.setVisibility(VISIBLE);
   1150         mImagePromptView.setText(R.string.show_images);
   1151         mImagePromptView.setTag(SHOW_IMAGE_PROMPT_ONCE);
   1152     }
   1153 
   1154     /**
   1155      * Shows the "Always show pictures" message
   1156      *
   1157      * @param initialShowing <code>true</code> if this is the first time we are showing the prompt
   1158      *        for "show images", <code>false</code> if we are transitioning from "Show pictures"
   1159      */
   1160     private void showImagePromptAlways(final boolean initialShowing) {
   1161         if (initialShowing) {
   1162             // Initialize the view
   1163             showImagePromptOnce();
   1164         }
   1165 
   1166         mImagePromptView.setText(R.string.always_show_images);
   1167         mImagePromptView.setTag(SHOW_IMAGE_PROMPT_ALWAYS);
   1168 
   1169         if (!initialShowing) {
   1170             // the new text's line count may differ, so update the spacer height
   1171             updateSpacerHeight();
   1172         }
   1173     }
   1174 
   1175     private void hideSpamWarning() {
   1176         if (mSpamWarningView != null) {
   1177             mSpamWarningView.setVisibility(GONE);
   1178         }
   1179     }
   1180 
   1181     private void showSpamWarning() {
   1182         if (mSpamWarningView == null) {
   1183             mSpamWarningView = (SpamWarningView)
   1184                     mInflater.inflate(R.layout.conversation_message_spam_warning, this, false);
   1185             mExtraContentView.addView(mSpamWarningView);
   1186         }
   1187 
   1188         mSpamWarningView.showSpamWarning(mMessage, mSender);
   1189     }
   1190 
   1191     private void handleShowImagePromptClick(View v) {
   1192         Integer state = (Integer) v.getTag();
   1193         if (state == null) {
   1194             return;
   1195         }
   1196         switch (state) {
   1197             case SHOW_IMAGE_PROMPT_ONCE:
   1198                 if (mCallbacks != null) {
   1199                     mCallbacks.showExternalResources(mMessage);
   1200                 }
   1201                 if (mMessageHeaderItem != null) {
   1202                     mMessageHeaderItem.setShowImages(true);
   1203                 }
   1204                 if (mIsViewOnlyMode) {
   1205                     hideShowImagePrompt();
   1206                 } else {
   1207                     showImagePromptAlways(false);
   1208                 }
   1209                 break;
   1210             case SHOW_IMAGE_PROMPT_ALWAYS:
   1211                 mMessage.markAlwaysShowImages(getQueryHandler(), 0 /* token */, null /* cookie */);
   1212 
   1213                 if (mCallbacks != null) {
   1214                     mCallbacks.showExternalResources(mMessage.getFrom());
   1215                 }
   1216 
   1217                 mShowImagePrompt = false;
   1218                 v.setTag(null);
   1219                 v.setVisibility(GONE);
   1220                 updateSpacerHeight();
   1221                 Toast.makeText(getContext(), R.string.always_show_images_toast, Toast.LENGTH_SHORT)
   1222                         .show();
   1223                 break;
   1224         }
   1225     }
   1226 
   1227     private AsyncQueryHandler getQueryHandler() {
   1228         if (mQueryHandler == null) {
   1229             mQueryHandler = new AsyncQueryHandler(getContext().getContentResolver()) {};
   1230         }
   1231         return mQueryHandler;
   1232     }
   1233 
   1234     /**
   1235      * Makes expanded details visible. If necessary, will inflate expanded
   1236      * details layout and render using saved-off state (senders, timestamp,
   1237      * etc).
   1238      */
   1239     private void showExpandedDetails() {
   1240         // lazily create expanded details view
   1241         final boolean expandedViewCreated = ensureExpandedDetailsView();
   1242         if (expandedViewCreated) {
   1243             mExtraContentView.addView(mExpandedDetailsView, 0);
   1244         }
   1245         mExpandedDetailsView.setVisibility(VISIBLE);
   1246         mDateView.setVisibility(GONE);
   1247         mHideDetailsView.setVisibility(VISIBLE);
   1248     }
   1249 
   1250     private boolean ensureExpandedDetailsView() {
   1251         boolean viewCreated = false;
   1252         if (mExpandedDetailsView == null) {
   1253             View v = inflateExpandedDetails(mInflater);
   1254             v.setOnClickListener(this);
   1255 
   1256             mExpandedDetailsView = (ViewGroup) v;
   1257             viewCreated = true;
   1258         }
   1259         if (!mExpandedDetailsValid) {
   1260             renderExpandedDetails(getResources(), mExpandedDetailsView, mMessage.viaDomain,
   1261                     mAddressCache, getAccount(), mVeiledMatcher, mFrom, mReplyTo, mTo, mCc, mBcc,
   1262                     mMessageHeaderItem.getTimestampFull(),
   1263                     getBidiFormatter());
   1264 
   1265             mExpandedDetailsValid = true;
   1266         }
   1267         return viewCreated;
   1268     }
   1269 
   1270     public static View inflateExpandedDetails(LayoutInflater inflater) {
   1271         return inflater.inflate(R.layout.conversation_message_header_details, null, false);
   1272     }
   1273 
   1274     public static void renderExpandedDetails(Resources res, View detailsView,
   1275             String viaDomain, Map<String, Address> addressCache, Account account,
   1276             VeiledAddressMatcher veiledMatcher, String[] from, String[] replyTo,
   1277             String[] to, String[] cc, String[] bcc, CharSequence receivedTimestamp,
   1278             BidiFormatter bidiFormatter) {
   1279         renderEmailList(res, R.id.from_heading, R.id.from_details, from, viaDomain,
   1280                 detailsView, addressCache, account, veiledMatcher, bidiFormatter);
   1281         renderEmailList(res, R.id.replyto_heading, R.id.replyto_details, replyTo, viaDomain,
   1282                 detailsView, addressCache, account, veiledMatcher, bidiFormatter);
   1283         renderEmailList(res, R.id.to_heading, R.id.to_details, to, viaDomain,
   1284                 detailsView, addressCache, account, veiledMatcher, bidiFormatter);
   1285         renderEmailList(res, R.id.cc_heading, R.id.cc_details, cc, viaDomain,
   1286                 detailsView, addressCache, account, veiledMatcher, bidiFormatter);
   1287         renderEmailList(res, R.id.bcc_heading, R.id.bcc_details, bcc, viaDomain,
   1288                 detailsView, addressCache, account, veiledMatcher, bidiFormatter);
   1289 
   1290         // Render date
   1291         detailsView.findViewById(R.id.date_heading).setVisibility(VISIBLE);
   1292         final TextView date = (TextView) detailsView.findViewById(R.id.date_details);
   1293         date.setText(receivedTimestamp);
   1294         date.setVisibility(VISIBLE);
   1295     }
   1296 
   1297     /**
   1298      * Render an email list for the expanded message details view.
   1299      */
   1300     private static void renderEmailList(Resources res, int headerId, int detailsId,
   1301             String[] emails, String viaDomain, View rootView,
   1302             Map<String, Address> addressCache, Account account,
   1303             VeiledAddressMatcher veiledMatcher, BidiFormatter bidiFormatter) {
   1304         if (emails == null || emails.length == 0) {
   1305             return;
   1306         }
   1307         final String[] formattedEmails = new String[emails.length];
   1308         for (int i = 0; i < emails.length; i++) {
   1309             final Address email = Utils.getAddress(addressCache, emails[i]);
   1310             String name = email.getPersonal();
   1311             final String address = email.getAddress();
   1312             // Check if the address here is a veiled address.  If it is, we need to display an
   1313             // alternate layout
   1314             final boolean isVeiledAddress = veiledMatcher != null &&
   1315                     veiledMatcher.isVeiledAddress(address);
   1316             final String addressShown;
   1317             if (isVeiledAddress) {
   1318                 // Add the warning at the end of the name, and remove the address.  The alternate
   1319                 // text cannot be put in the address part, because the address is made into a link,
   1320                 // and the alternate human-readable text is not a link.
   1321                 addressShown = "";
   1322                 if (TextUtils.isEmpty(name)) {
   1323                     // Empty name and we will block out the address. Let's write something more
   1324                     // readable.
   1325                     name = res.getString(VeiledAddressMatcher.VEILED_ALTERNATE_TEXT_UNKNOWN_PERSON);
   1326                 } else {
   1327                     name = name + res.getString(VeiledAddressMatcher.VEILED_ALTERNATE_TEXT);
   1328                 }
   1329             } else {
   1330                 addressShown = address;
   1331             }
   1332             if (name == null || name.length() == 0 || name.equalsIgnoreCase(addressShown)) {
   1333                 formattedEmails[i] = bidiFormatter.unicodeWrap(addressShown);
   1334             } else {
   1335                 // The one downside to having the showViaDomain here is that
   1336                 // if the sender does not have a name, it will not show the via info
   1337                 if (viaDomain != null) {
   1338                     formattedEmails[i] = res.getString(
   1339                             R.string.address_display_format_with_via_domain,
   1340                             bidiFormatter.unicodeWrap(name),
   1341                             bidiFormatter.unicodeWrap(addressShown),
   1342                             bidiFormatter.unicodeWrap(viaDomain));
   1343                 } else {
   1344                     formattedEmails[i] = res.getString(R.string.address_display_format,
   1345                             bidiFormatter.unicodeWrap(name),
   1346                             bidiFormatter.unicodeWrap(addressShown));
   1347                 }
   1348             }
   1349         }
   1350 
   1351         rootView.findViewById(headerId).setVisibility(VISIBLE);
   1352         final TextView detailsText = (TextView) rootView.findViewById(detailsId);
   1353         detailsText.setText(TextUtils.join("\n", formattedEmails));
   1354         stripUnderlines(detailsText, account);
   1355         detailsText.setVisibility(VISIBLE);
   1356     }
   1357 
   1358     private static void stripUnderlines(TextView textView, Account account) {
   1359         final Spannable spannable = (Spannable) textView.getText();
   1360         final URLSpan[] urls = textView.getUrls();
   1361 
   1362         for (URLSpan span : urls) {
   1363             final int start = spannable.getSpanStart(span);
   1364             final int end = spannable.getSpanEnd(span);
   1365             spannable.removeSpan(span);
   1366             span = new EmailAddressSpan(account, span.getURL().substring(7));
   1367             spannable.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
   1368         }
   1369     }
   1370 
   1371     /**
   1372      * Returns a short plaintext snippet generated from the given HTML message
   1373      * body. Collapses whitespace, ignores '&lt;' and '&gt;' characters and
   1374      * everything in between, and truncates the snippet to no more than 100
   1375      * characters.
   1376      *
   1377      * @return Short plaintext snippet
   1378      */
   1379     @VisibleForTesting
   1380     static String makeSnippet(final String messageBody) {
   1381         if (TextUtils.isEmpty(messageBody)) {
   1382             return null;
   1383         }
   1384 
   1385         final StringBuilder snippet = new StringBuilder(MAX_SNIPPET_LENGTH);
   1386 
   1387         final StringReader reader = new StringReader(messageBody);
   1388         try {
   1389             int c;
   1390             while ((c = reader.read()) != -1 && snippet.length() < MAX_SNIPPET_LENGTH) {
   1391                 // Collapse whitespace.
   1392                 if (Character.isWhitespace(c)) {
   1393                     snippet.append(' ');
   1394                     do {
   1395                         c = reader.read();
   1396                     } while (Character.isWhitespace(c));
   1397                     if (c == -1) {
   1398                         break;
   1399                     }
   1400                 }
   1401 
   1402                 if (c == '<') {
   1403                     // Ignore everything up to and including the next '>'
   1404                     // character.
   1405                     while ((c = reader.read()) != -1) {
   1406                         if (c == '>') {
   1407                             break;
   1408                         }
   1409                     }
   1410 
   1411                     // If we reached the end of the message body, exit.
   1412                     if (c == -1) {
   1413                         break;
   1414                     }
   1415                 } else if (c == '&') {
   1416                     // Read HTML entity.
   1417                     StringBuilder sb = new StringBuilder();
   1418 
   1419                     while ((c = reader.read()) != -1) {
   1420                         if (c == ';') {
   1421                             break;
   1422                         }
   1423                         sb.append((char) c);
   1424                     }
   1425 
   1426                     String entity = sb.toString();
   1427                     if ("nbsp".equals(entity)) {
   1428                         snippet.append(' ');
   1429                     } else if ("lt".equals(entity)) {
   1430                         snippet.append('<');
   1431                     } else if ("gt".equals(entity)) {
   1432                         snippet.append('>');
   1433                     } else if ("amp".equals(entity)) {
   1434                         snippet.append('&');
   1435                     } else if ("quot".equals(entity)) {
   1436                         snippet.append('"');
   1437                     } else if ("apos".equals(entity) || "#39".equals(entity)) {
   1438                         snippet.append('\'');
   1439                     } else {
   1440                         // Unknown entity; just append the literal string.
   1441                         snippet.append('&').append(entity);
   1442                         if (c == ';') {
   1443                             snippet.append(';');
   1444                         }
   1445                     }
   1446 
   1447                     // If we reached the end of the message body, exit.
   1448                     if (c == -1) {
   1449                         break;
   1450                     }
   1451                 } else {
   1452                     // The current character is a non-whitespace character that
   1453                     // isn't inside some
   1454                     // HTML tag and is not part of an HTML entity.
   1455                     snippet.append((char) c);
   1456                 }
   1457             }
   1458         } catch (IOException e) {
   1459             LogUtils.wtf(LOG_TAG, e, "Really? IOException while reading a freaking string?!? ");
   1460         }
   1461 
   1462         return snippet.toString();
   1463     }
   1464 
   1465     @Override
   1466     protected void onLayout(boolean changed, int l, int t, int r, int b) {
   1467         Timer perf = new Timer();
   1468         perf.start(LAYOUT_TAG);
   1469         super.onLayout(changed, l, t, r, b);
   1470         perf.pause(LAYOUT_TAG);
   1471     }
   1472 
   1473     @Override
   1474     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   1475         Timer t = new Timer();
   1476         if (Timer.ENABLE_TIMER && !mPreMeasuring) {
   1477             t.count("header measure id=" + mMessage.id);
   1478             t.start(MEASURE_TAG);
   1479         }
   1480         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
   1481         if (!mPreMeasuring) {
   1482             t.pause(MEASURE_TAG);
   1483         }
   1484     }
   1485 }
   1486