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