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