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