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
17package com.android.mail.browse;
18
19import android.annotation.SuppressLint;
20import android.app.FragmentManager;
21import android.content.AsyncQueryHandler;
22import android.content.Context;
23import android.content.res.Resources;
24import android.database.DataSetObserver;
25import android.graphics.Bitmap;
26import android.support.v4.text.BidiFormatter;
27import android.text.Html;
28import android.text.Spannable;
29import android.text.Spanned;
30import android.text.TextUtils;
31import android.text.method.LinkMovementMethod;
32import android.text.style.URLSpan;
33import android.util.AttributeSet;
34import android.view.LayoutInflater;
35import android.view.Menu;
36import android.view.MenuItem;
37import android.view.View;
38import android.view.View.OnClickListener;
39import android.view.ViewGroup;
40import android.widget.PopupMenu;
41import android.widget.PopupMenu.OnMenuItemClickListener;
42import android.widget.QuickContactBadge;
43import android.widget.TextView;
44import android.widget.Toast;
45
46import com.android.emailcommon.mail.Address;
47import com.android.mail.ContactInfo;
48import com.android.mail.ContactInfoSource;
49import com.android.mail.R;
50import com.android.mail.analytics.Analytics;
51import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem;
52import com.android.mail.compose.ComposeActivity;
53import com.android.mail.perf.Timer;
54import com.android.mail.photomanager.LetterTileProvider;
55import com.android.mail.print.PrintUtils;
56import com.android.mail.providers.Account;
57import com.android.mail.providers.Conversation;
58import com.android.mail.providers.Message;
59import com.android.mail.providers.Settings;
60import com.android.mail.providers.UIProvider;
61import com.android.mail.text.EmailAddressSpan;
62import com.android.mail.ui.AbstractConversationViewFragment;
63import com.android.mail.ui.ImageCanvas;
64import com.android.mail.utils.BitmapUtil;
65import com.android.mail.utils.LogTag;
66import com.android.mail.utils.LogUtils;
67import com.android.mail.utils.StyleUtils;
68import com.android.mail.utils.Utils;
69import com.android.mail.utils.VeiledAddressMatcher;
70import com.google.common.annotations.VisibleForTesting;
71
72import java.io.IOException;
73import java.io.StringReader;
74import java.util.Map;
75
76public 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