• 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.support.v4.text.BidiFormatter;
27 import android.text.Html;
28 import android.text.Spannable;
29 import android.text.Spanned;
30 import android.text.TextUtils;
31 import android.text.method.LinkMovementMethod;
32 import android.text.style.URLSpan;
33 import android.util.AttributeSet;
34 import android.view.LayoutInflater;
35 import android.view.Menu;
36 import android.view.MenuItem;
37 import android.view.View;
38 import android.view.View.OnClickListener;
39 import android.view.ViewGroup;
40 import android.widget.PopupMenu;
41 import android.widget.PopupMenu.OnMenuItemClickListener;
42 import android.widget.QuickContactBadge;
43 import android.widget.TextView;
44 import android.widget.Toast;
45 
46 import com.android.emailcommon.mail.Address;
47 import com.android.mail.ContactInfo;
48 import com.android.mail.ContactInfoSource;
49 import com.android.mail.R;
50 import com.android.mail.analytics.Analytics;
51 import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem;
52 import com.android.mail.compose.ComposeActivity;
53 import com.android.mail.perf.Timer;
54 import com.android.mail.photomanager.LetterTileProvider;
55 import com.android.mail.print.PrintUtils;
56 import com.android.mail.providers.Account;
57 import com.android.mail.providers.Conversation;
58 import com.android.mail.providers.Message;
59 import com.android.mail.providers.Settings;
60 import com.android.mail.providers.UIProvider;
61 import com.android.mail.text.EmailAddressSpan;
62 import com.android.mail.ui.AbstractConversationViewFragment;
63 import com.android.mail.ui.ImageCanvas;
64 import com.android.mail.utils.BitmapUtil;
65 import com.android.mail.utils.LogTag;
66 import com.android.mail.utils.LogUtils;
67 import com.android.mail.utils.StyleUtils;
68 import com.android.mail.utils.Utils;
69 import com.android.mail.utils.VeiledAddressMatcher;
70 import com.google.common.annotations.VisibleForTesting;
71 
72 import java.io.IOException;
73 import java.io.StringReader;
74 import java.util.Map;
75 
76 public class MessageHeaderView extends SnapHeader implements OnClickListener,
77         OnMenuItemClickListener, ConversationContainer.DetachListener {
78 
79     /**
80      * Cap very long recipient lists during summary construction for efficiency.
81      */
82     private static final int SUMMARY_MAX_RECIPIENTS = 50;
83 
84     private static final int MAX_SNIPPET_LENGTH = 100;
85 
86     private static final int SHOW_IMAGE_PROMPT_ONCE = 1;
87     private static final int SHOW_IMAGE_PROMPT_ALWAYS = 2;
88 
89     private static final String HEADER_RENDER_TAG = "message header render";
90     private static final String LAYOUT_TAG = "message header layout";
91     private static final String MEASURE_TAG = "message header measure";
92 
93     private static final String LOG_TAG = LogTag.getLogTag();
94 
95     // This is a debug only feature
96     public static final boolean ENABLE_REPORT_RENDERING_PROBLEM = false;
97 
98     private MessageHeaderViewCallbacks mCallbacks;
99 
100     private View mBorderView;
101     private ViewGroup mUpperHeaderView;
102     private View mTitleContainer;
103     private View mSnapHeaderBottomBorder;
104     private TextView mSenderNameView;
105     private TextView mRecipientSummary;
106     private TextView mDateView;
107     private View mHideDetailsView;
108     private TextView mSnippetView;
109     private MessageHeaderContactBadge mPhotoView;
110     private ViewGroup mExtraContentView;
111     private ViewGroup mExpandedDetailsView;
112     private SpamWarningView mSpamWarningView;
113     private TextView mImagePromptView;
114     private MessageInviteView mInviteView;
115     private View mForwardButton;
116     private View mOverflowButton;
117     private View mDraftIcon;
118     private View mEditDraftButton;
119     private TextView mUpperDateView;
120     private View mReplyButton;
121     private View mReplyAllButton;
122     private View mAttachmentIcon;
123     private final EmailCopyContextMenu mEmailCopyMenu;
124 
125     // temporary fields to reference raw data between initial render and details
126     // expansion
127     private String[] mFrom;
128     private String[] mTo;
129     private String[] mCc;
130     private String[] mBcc;
131     private String[] mReplyTo;
132 
133     private boolean mIsDraft = false;
134 
135     private int mSendingState;
136 
137     private String mSnippet;
138 
139     private Address mSender;
140 
141     private ContactInfoSource mContactInfoSource;
142 
143     private boolean mPreMeasuring;
144 
145     private ConversationAccountController mAccountController;
146 
147     private Map<String, Address> mAddressCache;
148 
149     private boolean mShowImagePrompt;
150 
151     private PopupMenu mPopup;
152 
153     private MessageHeaderItem mMessageHeaderItem;
154     private ConversationMessage mMessage;
155 
156     private boolean mRecipientSummaryValid;
157     private boolean mExpandedDetailsValid;
158 
159     private final LayoutInflater mInflater;
160 
161     private AsyncQueryHandler mQueryHandler;
162 
163     private boolean mObservingContactInfo;
164 
165     /**
166      * What I call myself? "me" in English, and internationalized correctly.
167      */
168     private final String mMyName;
169 
170     private final DataSetObserver mContactInfoObserver = new DataSetObserver() {
171         @Override
172         public void onChanged() {
173             updateContactInfo();
174         }
175     };
176 
177     private boolean mExpandable = true;
178 
179     private VeiledAddressMatcher mVeiledMatcher;
180 
181     private boolean mIsViewOnlyMode = false;
182 
183     private LetterTileProvider mLetterTileProvider;
184     private final int mContactPhotoWidth;
185     private final int mContactPhotoHeight;
186     private final int mTitleContainerMarginEnd;
187 
188     /**
189      * The snappy header has special visibility rules (i.e. no details header,
190      * even though it has an expanded appearance)
191      */
192     private boolean mIsSnappy;
193 
194     private BidiFormatter mBidiFormatter;
195 
196 
197     public interface MessageHeaderViewCallbacks {
setMessageSpacerHeight(MessageHeaderItem item, int newSpacerHeight)198         void setMessageSpacerHeight(MessageHeaderItem item, int newSpacerHeight);
199 
setMessageExpanded(MessageHeaderItem item, int newSpacerHeight)200         void setMessageExpanded(MessageHeaderItem item, int newSpacerHeight);
201 
setMessageDetailsExpanded(MessageHeaderItem messageHeaderItem, boolean expanded, int previousMessageHeaderItemHeight)202         void setMessageDetailsExpanded(MessageHeaderItem messageHeaderItem, boolean expanded,
203                 int previousMessageHeaderItemHeight);
204 
showExternalResources(Message msg)205         void showExternalResources(Message msg);
206 
showExternalResources(String senderRawAddress)207         void showExternalResources(String senderRawAddress);
208 
supportsMessageTransforms()209         boolean supportsMessageTransforms();
210 
getMessageTransforms(Message msg)211         String getMessageTransforms(Message msg);
212 
getFragmentManager()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          */
isSecure()219         boolean isSecure();
220     }
221 
MessageHeaderView(Context context)222     public MessageHeaderView(Context context) {
223         this(context, null);
224     }
225 
MessageHeaderView(Context context, AttributeSet attrs)226     public MessageHeaderView(Context context, AttributeSet attrs) {
227         this(context, attrs, -1);
228     }
229 
MessageHeaderView(Context context, AttributeSet attrs, int defStyle)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
onFinishInflate()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 
registerMessageClickTargets(View... views)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
initialize(ConversationAccountController accountController, Map<String, Address> addressCache, MessageHeaderViewCallbacks callbacks, ContactInfoSource contactInfoSource, VeiledAddressMatcher veiledAddressMatcher)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      */
setContactInfoSource(ContactInfoSource contactInfoSource)299     public void setContactInfoSource(ContactInfoSource contactInfoSource) {
300         mContactInfoSource = contactInfoSource;
301     }
302 
setCallbacks(MessageHeaderViewCallbacks callbacks)303     public void setCallbacks(MessageHeaderViewCallbacks callbacks) {
304         mCallbacks = callbacks;
305     }
306 
setVeiledMatcher(VeiledAddressMatcher matcher)307     public void setVeiledMatcher(VeiledAddressMatcher matcher) {
308         mVeiledMatcher = matcher;
309     }
310 
isExpanded()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
onDetachedFromParent()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
unbind()328     public void unbind() {
329         mMessageHeaderItem = null;
330         mMessage = null;
331 
332         if (mObservingContactInfo) {
333             mContactInfoSource.unregisterObserver(mContactInfoObserver);
334             mObservingContactInfo = false;
335         }
336     }
337 
initialize(ConversationAccountController accountController, Map<String, Address> addressCache)338     public void initialize(ConversationAccountController accountController,
339             Map<String, Address> addressCache) {
340         mAccountController = accountController;
341         mAddressCache = addressCache;
342     }
343 
getAccount()344     private Account getAccount() {
345         return mAccountController != null ? mAccountController.getAccount() : null;
346     }
347 
bind(MessageHeaderItem headerItem, boolean measureOnly)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      */
rebind(MessageHeaderItem headerItem)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
refresh()373     public void refresh() {
374         render(false);
375     }
376 
getBidiFormatter()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 
render(boolean measureOnly)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      */
setAddressOnContextMenu()487     private void setAddressOnContextMenu() {
488         if (mSender != null) {
489             mEmailCopyMenu.setAddress(mSender.getAddress());
490         }
491     }
492 
493     @Override
isBoundTo(ConversationOverlayItem item)494     public boolean isBoundTo(ConversationOverlayItem item) {
495         return item == mMessageHeaderItem;
496     }
497 
getAddress(String emailStr)498     public Address getAddress(String emailStr) {
499         return Utils.getAddress(mAddressCache, emailStr);
500     }
501 
updateSpacerHeight()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 
measureHeight()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 
getHeaderTitle()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 
setRecipientSummary()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 
setDateText()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      */
getSenderName(Address sender)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 
setChildVisibility(int visibility, View... children)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 
setExpanded(final boolean expanded)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      */
updateChildVisibility()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      */
setReplyOrReplyAllVisible()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")
setChildMarginEnd(View childView, int marginEnd)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
getRecipientSummaryText(Context context, String meEmailAddress, String myName, String[] to, String[] cc, String[] bcc, Map<String, Address> addressCache, VeiledAddressMatcher matcher, BidiFormatter bidiFormatter)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 
RecipientListsBuilder(Context context, String meEmailAddress, String myName, Map<String, Address> addressCache, VeiledAddressMatcher matcher, BidiFormatter bidiFormatter)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 
append(String[] recipients)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 
appendBcc(String[] recipients)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          */
appendRecipients(String[] rawAddrs, int maxToCopy)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          */
shouldAppendRecipients(String[] rawAddrs, int maxToCopy)821         private boolean shouldAppendRecipients(String[] rawAddrs, int maxToCopy) {
822             return rawAddrs != null && rawAddrs.length != 0 && maxToCopy != 0;
823         }
824 
build()825         public CharSequence build() {
826             return mContext.getString(R.string.to_message_header, mBuilder);
827         }
828     }
829 
updateContactInfo()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 
makeLetterTile( String displayName, String senderAddress)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
onMenuItemClick(MenuItem item)882     public boolean onMenuItemClick(MenuItem item) {
883         mPopup.dismiss();
884         return onClick(null, item.getItemId());
885     }
886 
887     @Override
onClick(View v)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      */
onClick(final View v, final int id)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 
printMessage()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      */
setViewOnlyMode(boolean isViewOnlyMode)993     public void setViewOnlyMode(boolean isViewOnlyMode) {
994         mIsViewOnlyMode = isViewOnlyMode;
995     }
996 
setExpandable(boolean expandable)997     public void setExpandable(boolean expandable) {
998         mExpandable = expandable;
999     }
1000 
toggleExpanded()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 
isValidPosition(int position, int size)1027     private static boolean isValidPosition(int position, int size) {
1028         return position >= 0 && position < size;
1029     }
1030 
1031     @Override
setSnappy()1032     public void setSnappy() {
1033         mIsSnappy = true;
1034         hideMessageDetails();
1035     }
1036 
isSnappy()1037     private boolean isSnappy() {
1038         return mIsSnappy;
1039     }
1040 
toggleMessageDetails()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 
setMessageDetailsExpanded(boolean expand)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 
setMessageDetailsVisibility(int vis)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 
hideMessageDetails()1096     private void hideMessageDetails() {
1097         setMessageDetailsVisibility(GONE);
1098     }
1099 
hideExpandedDetails()1100     private void hideExpandedDetails() {
1101         if (mExpandedDetailsView != null) {
1102             mExpandedDetailsView.setVisibility(GONE);
1103         }
1104         mDateView.setVisibility(VISIBLE);
1105         mHideDetailsView.setVisibility(GONE);
1106     }
1107 
hideInvite()1108     private void hideInvite() {
1109         if (mInviteView != null) {
1110             mInviteView.setVisibility(GONE);
1111         }
1112     }
1113 
showInvite()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 
hideShowImagePrompt()1124     private void hideShowImagePrompt() {
1125         if (mImagePromptView != null) {
1126             mImagePromptView.setVisibility(GONE);
1127         }
1128     }
1129 
showImagePromptOnce()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      */
showImagePromptAlways(final boolean initialShowing)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 
hideSpamWarning()1163     private void hideSpamWarning() {
1164         if (mSpamWarningView != null) {
1165             mSpamWarningView.setVisibility(GONE);
1166         }
1167     }
1168 
showSpamWarning()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 
handleShowImagePromptClick(View v)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 
getQueryHandler()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      */
showExpandedDetails()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 
ensureExpandedDetailsView()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 
inflateExpandedDetails(LayoutInflater inflater)1258     public static View inflateExpandedDetails(LayoutInflater inflater) {
1259         return inflater.inflate(R.layout.conversation_message_header_details, null, false);
1260     }
1261 
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)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      */
renderEmailList(Resources res, int headerId, int detailsId, String[] emails, String viaDomain, View rootView, Map<String, Address> addressCache, Account account, VeiledAddressMatcher veiledMatcher, BidiFormatter bidiFormatter)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 
stripUnderlines(TextView textView, Account account)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
makeSnippet(final String messageBody)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
onLayout(boolean changed, int l, int t, int r, int b)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
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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