Home | History | Annotate | Download | only in browse
      1 /**
      2  * Copyright (c) 2011, Google Inc.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *     http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.mail.browse;
     18 
     19 import android.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 {
    219         void setMessageSpacerHeight(MessageHeaderItem item, int newSpacerHeight);
    220 
    221         void setMessageExpanded(MessageHeaderItem item, int newSpacerHeight,
    222                 int topBorderHeight, int bottomBorderHeight);
    223 
    224         void setMessageDetailsExpanded(MessageHeaderItem messageHeaderItem, boolean expanded,
    225                 int previousMessageHeaderItemHeight);
    226 
    227         void showExternalResources(Message msg);
    228 
    229         void showExternalResources(String senderRawAddress);
    230 
    231         boolean supportsMessageTransforms();
    232 
    233         String getMessageTransforms(Message msg);
    234 
    235         FragmentManager getFragmentManager();
    236     }
    237 
    238     public MessageHeaderView(Context context) {
    239         this(context, null);
    240     }
    241 
    242     public MessageHeaderView(Context context, AttributeSet attrs) {
    243         this(context, attrs, -1);
    244     }
    245 
    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      */
    263     public void setExpandMode(int mode) {
    264         mExpandMode = mode;
    265     }
    266 
    267     @Override
    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 
    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      */
    315     public void setContactInfoSource(ContactInfoSource contactInfoSource) {
    316         mContactInfoSource = contactInfoSource;
    317     }
    318 
    319     public void setCallbacks(MessageHeaderViewCallbacks callbacks) {
    320         mCallbacks = callbacks;
    321     }
    322 
    323     public void setVeiledMatcher(VeiledAddressMatcher matcher) {
    324         mVeiledMatcher = matcher;
    325     }
    326 
    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 
    332     public void setSnappy(boolean snappy) {
    333         mIsSnappy = snappy;
    334         hideMessageDetails();
    335     }
    336 
    337     @Override
    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      */
    348     public void unbind() {
    349         mMessageHeaderItem = null;
    350         mMessage = null;
    351 
    352         if (mObservingContactInfo) {
    353             mContactInfoSource.unregisterObserver(mContactInfoObserver);
    354             mObservingContactInfo = false;
    355         }
    356     }
    357 
    358     public void initialize(ConversationAccountController accountController,
    359             Map<String, Address> addressCache) {
    360         mAccountController = accountController;
    361         mAddressCache = addressCache;
    362     }
    363 
    364     private Account getAccount() {
    365         return mAccountController != null ? mAccountController.getAccount() : null;
    366     }
    367 
    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      */
    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 
    392     public void refresh() {
    393         render(false);
    394     }
    395 
    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      */
    489     private void setAddressOnContextMenu() {
    490         mEmailCopyMenu.setAddress(mSender.getAddress());
    491     }
    492 
    493     public boolean isBoundTo(ConversationOverlayItem item) {
    494         return item == mMessageHeaderItem;
    495     }
    496 
    497     public Address getAddress(String emailStr) {
    498         return getAddress(mAddressCache, emailStr);
    499     }
    500 
    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 
    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 
    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 
    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 
    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      */
    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      */
    582     private static CharSequence getSenderAddress(Address sender) {
    583         return sender.getAddress();
    584     }
    585 
    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 
    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      */
    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      */
    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 
    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 
    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 
    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 
    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 
    789         public CharSequence build() {
    790             return mBuilder;
    791         }
    792     }
    793 
    794     @VisibleForTesting
    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 
    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 
    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
    854     public boolean onMenuItemClick(MenuItem item) {
    855         mPopup.dismiss();
    856         return onClick(null, item.getItemId());
    857     }
    858 
    859     @Override
    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      */
    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      */
    935     public void setViewOnlyMode(boolean isViewOnlyMode) {
    936         mIsViewOnlyMode = isViewOnlyMode;
    937     }
    938 
    939     public void setExpandable(boolean expandable) {
    940         mExpandable = expandable;
    941     }
    942 
    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      */
    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 
   1032     private boolean isValidPosition(int position, int size) {
   1033         return position >= 0 && position < size;
   1034     }
   1035 
   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 
   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 
   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 
   1101     public void hideMessageDetails() {
   1102         setMessageDetailsVisibility(GONE);
   1103     }
   1104 
   1105     private void hideCollapsedDetails() {
   1106         if (mCollapsedDetailsView != null) {
   1107             mCollapsedDetailsView.setVisibility(GONE);
   1108         }
   1109     }
   1110 
   1111     private void hideExpandedDetails() {
   1112         if (mExpandedDetailsView != null) {
   1113             mExpandedDetailsView.setVisibility(GONE);
   1114         }
   1115     }
   1116 
   1117     private void hideInvite() {
   1118         if (mInviteView != null) {
   1119             mInviteView.setVisibility(GONE);
   1120         }
   1121     }
   1122 
   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 
   1133     private void hideShowImagePrompt() {
   1134         if (mImagePromptView != null) {
   1135             mImagePromptView.setVisibility(GONE);
   1136         }
   1137     }
   1138 
   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      */
   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 
   1172     private void hideSpamWarning() {
   1173         if (mSpamWarningView != null) {
   1174             mSpamWarningView.setVisibility(GONE);
   1175         }
   1176     }
   1177 
   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 
   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 
   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      */
   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      */
   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 
   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 
   1290     public static View inflateExpandedDetails(LayoutInflater inflater) {
   1291         return inflater.inflate(R.layout.conversation_message_details_header_expanded, null,
   1292                 false);
   1293     }
   1294 
   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      */
   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 
   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 
   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 
   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
   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
   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
   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