MessageHeaderView.java revision 65fe28fa88daad08f3be4c084ca5b4eaa366d1a7
1/**
2 * Copyright (c) 2011, Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.mail.browse;
18
19import android.content.AsyncQueryHandler;
20import android.content.Context;
21import android.graphics.Canvas;
22import android.graphics.Typeface;
23import android.provider.ContactsContract;
24import android.text.Spannable;
25import android.text.SpannableStringBuilder;
26import android.text.TextUtils;
27import android.text.style.StyleSpan;
28import android.util.AttributeSet;
29import android.view.LayoutInflater;
30import android.view.MenuItem;
31import android.view.View;
32import android.view.View.OnClickListener;
33import android.view.ViewGroup;
34import android.widget.ImageView;
35import android.widget.LinearLayout;
36import android.widget.PopupMenu;
37import android.widget.PopupMenu.OnMenuItemClickListener;
38import android.widget.QuickContactBadge;
39import android.widget.TextView;
40import android.widget.Toast;
41
42import com.android.mail.ContactInfoSource;
43import com.android.mail.FormattedDateBuilder;
44import com.android.mail.R;
45import com.android.mail.SenderInfoLoader.ContactInfo;
46import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem;
47import com.android.mail.compose.ComposeActivity;
48import com.android.mail.perf.Timer;
49import com.android.mail.providers.Account;
50import com.android.mail.providers.Address;
51import com.android.mail.providers.Message;
52import com.android.mail.providers.UIProvider;
53import com.android.mail.utils.LogUtils;
54import com.android.mail.utils.Utils;
55import com.google.common.annotations.VisibleForTesting;
56
57import java.io.IOException;
58import java.io.StringReader;
59import java.util.Map;
60
61public class MessageHeaderView extends LinearLayout implements OnClickListener,
62        OnMenuItemClickListener, HeaderBlock {
63
64    /**
65     * Cap very long recipient lists during summary construction for efficiency.
66     */
67    private static final int SUMMARY_MAX_RECIPIENTS = 50;
68
69    private static final int MAX_SNIPPET_LENGTH = 100;
70
71    private static final int SHOW_IMAGE_PROMPT_ONCE = 1;
72    private static final int SHOW_IMAGE_PROMPT_ALWAYS = 2;
73
74    private static final String HEADER_INFLATE_TAG = "message header inflate";
75    private static final String HEADER_ADDVIEW_TAG = "message header addView";
76    private static final String HEADER_RENDER_TAG = "message header render";
77    private static final String PREMEASURE_TAG = "message header pre-measure";
78    private static final String LAYOUT_TAG = "message header layout";
79    private static final String MEASURE_TAG = "message header measure";
80
81    private static final String RECIPIENT_HEADING_DELIMITER = "   ";
82
83    private static final String LOG_TAG = new LogUtils().getLogTag();
84
85    private MessageHeaderViewCallbacks mCallbacks;
86
87    private ViewGroup mUpperHeaderView;
88    private TextView mSenderNameView;
89    private TextView mSenderEmailView;
90    private QuickContactBadge mPhotoView;
91    private ImageView mStarView;
92    private ViewGroup mTitleContainerView;
93    private ViewGroup mCollapsedDetailsView;
94    private ViewGroup mExpandedDetailsView;
95    private ViewGroup mImagePromptView;
96    private View mBottomBorderView;
97    private ImageView mPresenceView;
98    private View mPhotoSpacerView;
99    private View mForwardButton;
100    private View mOverflowButton;
101    private View mDraftIcon;
102    private View mEditDraftButton;
103    private TextView mUpperDateView;
104    private View mReplyButton;
105    private View mReplyAllButton;
106    private View mAttachmentIcon;
107
108    // temporary fields to reference raw data between initial render and details
109    // expansion
110    private String[] mTo;
111    private String[] mCc;
112    private String[] mBcc;
113    private String[] mReplyTo;
114    private long mTimestampMs;
115    private FormattedDateBuilder mDateBuilder;
116
117    private boolean mIsDraft = false;
118
119    private boolean mIsSending;
120
121    /**
122     * The snappy header has special visibility rules (i.e. no details header,
123     * even though it has an expanded appearance)
124     */
125    private boolean mIsSnappy;
126
127    private String mSnippet;
128
129    private Address mSender;
130
131    private ContactInfoSource mContactInfoSource;
132
133    private boolean mPreMeasuring;
134
135    private Account mAccount;
136
137    private Map<String, Address> mAddressCache;
138
139    private boolean mShowImagePrompt;
140
141    private boolean mDefaultReplyAll;
142
143    private int mDrawTranslateY;
144
145    private CharSequence mTimestampShort;
146
147    /**
148     * Take the initial visibility of the star view to mean its collapsed
149     * visibility. Star is always visible when expanded, but sometimes, like on
150     * phones, there isn't enough room to warrant showing star when collapsed.
151     */
152    private int mCollapsedStarVis;
153
154    /**
155     * Take the initial right margin of the header title container to mean its
156     * right margin when collapsed. There's currently no need for additional
157     * margin when expanded, but if that need ever arises, title_container can
158     * simply tack on some extra right padding.
159     */
160    private int mTitleContainerCollapsedMarginRight;
161
162    private PopupMenu mPopup;
163
164    private MessageHeaderItem mMessageHeaderItem;
165    private Message mMessage;
166
167    private boolean mCollapsedDetailsValid;
168    private boolean mExpandedDetailsValid;
169
170    private final LayoutInflater mInflater;
171
172    private AsyncQueryHandler mQueryHandler;
173
174    public interface MessageHeaderViewCallbacks {
175        void setMessageSpacerHeight(MessageHeaderItem item, int newSpacerHeight);
176
177        void setMessageExpanded(MessageHeaderItem item, int newSpacerHeight);
178
179        void showExternalResources(Message msg);
180    }
181
182    public MessageHeaderView(Context context) {
183        this(context, null);
184    }
185
186    public MessageHeaderView(Context context, AttributeSet attrs) {
187        this(context, attrs, -1);
188    }
189
190    public MessageHeaderView(Context context, AttributeSet attrs, int defStyle) {
191        super(context, attrs, defStyle);
192
193        mInflater = LayoutInflater.from(context);
194    }
195
196    @Override
197    protected void onFinishInflate() {
198        super.onFinishInflate();
199        mUpperHeaderView = (ViewGroup) findViewById(R.id.upper_header);
200        mSenderNameView = (TextView) findViewById(R.id.sender_name);
201        mSenderEmailView = (TextView) findViewById(R.id.sender_email);
202        mPhotoView = (QuickContactBadge) findViewById(R.id.photo);
203        mPhotoSpacerView = findViewById(R.id.photo_spacer);
204        mReplyButton = findViewById(R.id.reply);
205        mReplyAllButton = findViewById(R.id.reply_all);
206        mForwardButton = findViewById(R.id.forward);
207        mStarView = (ImageView) findViewById(R.id.star);
208        mPresenceView = (ImageView) findViewById(R.id.presence);
209        mTitleContainerView = (ViewGroup) findViewById(R.id.title_container);
210        mOverflowButton = findViewById(R.id.overflow);
211        mDraftIcon = findViewById(R.id.draft);
212        mEditDraftButton = findViewById(R.id.edit_draft);
213        mUpperDateView = (TextView) findViewById(R.id.upper_date);
214        mAttachmentIcon = findViewById(R.id.attachment);
215
216        mCollapsedStarVis = mStarView.getVisibility();
217        mTitleContainerCollapsedMarginRight = ((MarginLayoutParams) mTitleContainerView
218                .getLayoutParams()).rightMargin;
219
220        mBottomBorderView = findViewById(R.id.details_bottom_border);
221
222        setExpanded(true);
223
224        registerMessageClickTargets(R.id.reply, R.id.reply_all, R.id.forward, R.id.star,
225                R.id.edit_draft, R.id.overflow, R.id.upper_header);
226    }
227
228    private void registerMessageClickTargets(int... ids) {
229        for (int id : ids) {
230            View v = findViewById(id);
231            if (v != null) {
232                v.setOnClickListener(this);
233            }
234        }
235    }
236
237    /**
238     * Associate the header with a contact info source for later contact
239     * presence/photo lookup.
240     */
241    public void setContactInfoSource(ContactInfoSource contactInfoSource) {
242        mContactInfoSource = contactInfoSource;
243    }
244
245    public void setCallbacks(MessageHeaderViewCallbacks callbacks) {
246        mCallbacks = callbacks;
247    }
248
249    /**
250     * Find the header view corresponding to a message with given local ID.
251     *
252     * @param parent the view parent to search within
253     * @param localMessageId local message ID
254     * @return a header view or null
255     */
256    public static MessageHeaderView find(ViewGroup parent, long localMessageId) {
257        return (MessageHeaderView) parent.findViewWithTag(localMessageId);
258    }
259
260    public boolean isExpanded() {
261        // (let's just arbitrarily say that unbound views are expanded by default)
262        return mMessageHeaderItem == null || mMessageHeaderItem.isExpanded();
263    }
264
265    @Override
266    public boolean canSnap() {
267        return isExpanded();
268    }
269
270    @Override
271    public MessageHeaderView getSnapView() {
272        return this;
273    }
274
275    public void setSnappy(boolean snappy) {
276        mIsSnappy = snappy;
277        hideMessageDetails();
278        if (snappy) {
279            setBackgroundDrawable(null);
280        } else {
281            setBackgroundColor(android.R.color.white);
282        }
283    }
284
285    /**
286     * Check if this header's displayed data matches that of another header.
287     *
288     * @param other another header
289     * @return true if the headers are displaying data for the same message
290     */
291    public boolean matches(MessageHeaderView other) {
292        return other != null && mMessage != null && mMessage.equals(other.mMessage);
293    }
294
295    /**
296     * Headers that are unbound will not match any rendered header (matches()
297     * will return false). Unbinding is not guaranteed to *hide* the view's old
298     * data, though. To re-bind this header to message data, call render() or
299     * renderUpperHeaderFrom().
300     */
301    public void unbind() {
302        mMessageHeaderItem = null;
303        mMessage = null;
304    }
305
306    public void renderUpperHeaderFrom(MessageHeaderView other) {
307        mMessageHeaderItem = other.mMessageHeaderItem;
308        mMessage = other.mMessage;
309        mSender = other.mSender;
310        mDefaultReplyAll = other.mDefaultReplyAll;
311
312        mSenderNameView.setText(other.mSenderNameView.getText());
313        mSenderEmailView.setText(other.mSenderEmailView.getText());
314        mStarView.setSelected(other.mStarView.isSelected());
315        mStarView.setContentDescription(getResources().getString(
316                mStarView.isSelected() ? R.string.remove_star : R.string.add_star));
317
318        updateContactInfo();
319
320        mIsDraft = other.mIsDraft;
321        updateChildVisibility();
322    }
323
324    public void initialize(FormattedDateBuilder dateBuilder, Account account,
325            Map<String, Address> addressCache) {
326        mDateBuilder = dateBuilder;
327        mAccount = account;
328        mAddressCache = addressCache;
329    }
330
331    public void bind(MessageHeaderItem headerItem, boolean defaultReplyAll) {
332        Timer t = new Timer();
333        t.start(HEADER_RENDER_TAG);
334
335        mCollapsedDetailsValid = false;
336        mExpandedDetailsValid = false;
337
338        mMessageHeaderItem = headerItem;
339        mMessage = headerItem.message;
340        mShowImagePrompt = mMessage.shouldShowImagePrompt();
341        mDefaultReplyAll = defaultReplyAll;
342        setExpanded(headerItem.isExpanded());
343
344        mTimestampMs = mMessage.dateReceivedMs;
345        mTimestampShort = headerItem.timestampShort;
346        if (mTimestampShort == null) {
347            mTimestampShort = mDateBuilder.formatShortDate(mTimestampMs);
348            headerItem.timestampShort = mTimestampShort;
349        }
350
351        mTo = mMessage.getToAddresses();
352        mCc = mMessage.getCcAddresses();
353        mBcc = mMessage.getBccAddresses();
354        mReplyTo = mMessage.getReplyToAddresses();
355
356        /**
357         * Turns draft mode on or off. Draft mode hides message operations other
358         * than "edit", hides contact photo, hides presence, and changes the
359         * sender name to "Draft".
360         */
361        mIsDraft = mMessage.draftType != UIProvider.DraftType.NOT_A_DRAFT;
362        mIsSending = isInOutbox();
363
364        updateChildVisibility();
365
366        if (mIsDraft || isInOutbox()) {
367            mSnippet = makeSnippet(mMessage.snippet);
368        } else {
369            mSnippet = mMessage.snippet;
370        }
371
372        // If this was a sent message AND:
373        // 1. the account has a custom from, the cursor will populate the
374        // selected custom from as the fromAddress when a message is sent but
375        // not yet synced.
376        // 2. the account has no custom froms, fromAddress will be empty, and we
377        // can safely fall back and show the account name as sender since it's
378        // the only possible fromAddress.
379        String from = mMessage.from;
380        if (TextUtils.isEmpty(from)) {
381            from = mAccount.name;
382        }
383        mSender = getAddress(from);
384
385        mSenderNameView.setText(getHeaderTitle());
386        mSenderEmailView.setText(getHeaderSubtitle());
387
388        if (mUpperDateView != null) {
389            mUpperDateView.setText(mTimestampShort);
390        }
391
392        mStarView.setSelected(mMessage.starred);
393        mStarView.setContentDescription(getResources().getString(
394                mStarView.isSelected() ? R.string.remove_star : R.string.add_star));
395
396        updateContactInfo();
397
398        t.pause(HEADER_RENDER_TAG);
399    }
400
401    private Address getAddress(String emailStr) {
402        return getAddress(mAddressCache, emailStr);
403    }
404
405    private static Address getAddress(Map<String, Address> cache, String emailStr) {
406        Address addr = null;
407        if (cache != null) {
408            addr = cache.get(emailStr);
409        }
410        if (addr == null) {
411            addr = Address.getEmailAddress(emailStr);
412            if (cache != null) {
413                cache.put(emailStr, addr);
414            }
415        }
416        return addr;
417    }
418
419    private boolean isInOutbox() {
420        // TODO: what should this read? Folder info?
421        return false;
422    }
423
424    private void updateSpacerHeight() {
425        final int h = measureHeight();
426
427        mMessageHeaderItem.setHeight(h);
428        if (mCallbacks != null) {
429            mCallbacks.setMessageSpacerHeight(mMessageHeaderItem, h);
430        }
431    }
432
433    private int measureHeight() {
434        ViewGroup parent = (ViewGroup) getParent();
435        if (parent == null) {
436            LogUtils.e(LOG_TAG, new Error(), "Unable to measure height of detached header");
437            return getHeight();
438        }
439        mPreMeasuring = true;
440        final int h = Utils.measureViewHeight(this, parent);
441        mPreMeasuring = false;
442        return h;
443    }
444
445    private CharSequence getHeaderTitle() {
446        CharSequence title;
447
448        if (mIsDraft) {
449            title = getResources().getQuantityText(R.plurals.draft, 1);
450        } else if (mIsSending) {
451            title = getResources().getString(R.string.sending);
452        } else {
453            title = getSenderName(mSender);
454        }
455
456        return title;
457    }
458
459    private CharSequence getHeaderSubtitle() {
460        CharSequence sub;
461        if (mIsSending) {
462            sub = null;
463        } else {
464            sub = isExpanded() ? getSenderAddress(mSender) : mSnippet;
465        }
466        return sub;
467    }
468
469    /**
470     * Return the name, if known, or just the address.
471     */
472    private static CharSequence getSenderName(Address sender) {
473        final String displayName = sender.getName();
474        return TextUtils.isEmpty(displayName) ? sender.getAddress() : displayName;
475    }
476
477    /**
478     * Return the address, if a name is present, or null if not.
479     */
480    private static CharSequence getSenderAddress(Address sender) {
481        String displayName = sender == null ? "" : sender.getName();
482        return TextUtils.isEmpty(displayName) ? null : sender.getAddress();
483    }
484
485    private void setChildVisibility(int visibility, View... children) {
486        for (View v : children) {
487            if (v != null) {
488                v.setVisibility(visibility);
489            }
490        }
491    }
492
493    private void setExpanded(final boolean expanded) {
494        // use View's 'activated' flag to store expanded state
495        // child view state lists can use this to toggle drawables
496        setActivated(expanded);
497        if (mMessageHeaderItem != null) {
498            mMessageHeaderItem.setExpanded(expanded);
499        }
500    }
501
502    /**
503     * Update the visibility of the many child views based on expanded/collapsed
504     * and draft/normal state.
505     */
506    private void updateChildVisibility() {
507        // Too bad this can't be done with an XML state list...
508
509        if (isExpanded()) {
510            int normalVis, draftVis;
511
512            setMessageDetailsVisibility((mIsSnappy) ? GONE : VISIBLE);
513
514            if (mIsDraft) {
515                normalVis = GONE;
516                draftVis = VISIBLE;
517            } else {
518                normalVis = VISIBLE;
519                draftVis = GONE;
520            }
521
522            setReplyOrReplyAllVisible();
523            setChildVisibility(normalVis, mPhotoView, mPhotoSpacerView, mForwardButton,
524                    mSenderEmailView, mOverflowButton);
525            setChildVisibility(draftVis, mDraftIcon, mEditDraftButton);
526            setChildVisibility(GONE, mAttachmentIcon, mUpperDateView);
527            setChildVisibility(VISIBLE, mStarView);
528
529            setChildMarginRight(mTitleContainerView, 0);
530
531        } else {
532
533            setMessageDetailsVisibility(GONE);
534            setChildVisibility(VISIBLE, mSenderEmailView, mUpperDateView);
535
536            setChildVisibility(GONE, mEditDraftButton, mReplyButton, mReplyAllButton,
537                    mForwardButton);
538            setChildVisibility(GONE, mOverflowButton);
539
540            setChildVisibility(mMessage.hasAttachments ? VISIBLE : GONE,
541                    mAttachmentIcon);
542
543            setChildVisibility(mCollapsedStarVis, mStarView);
544
545            setChildMarginRight(mTitleContainerView, mTitleContainerCollapsedMarginRight);
546
547            if (mIsDraft) {
548
549                setChildVisibility(VISIBLE, mDraftIcon);
550                setChildVisibility(GONE, mPhotoView, mPhotoSpacerView);
551
552            } else {
553
554                setChildVisibility(GONE, mDraftIcon);
555                setChildVisibility(VISIBLE, mPhotoView, mPhotoSpacerView);
556
557            }
558        }
559
560    }
561
562    /**
563     * If an overflow menu is present in this header's layout, set the
564     * visibility of "Reply" and "Reply All" actions based on a user preference.
565     * Only one of those actions will be visible when an overflow is present. If
566     * no overflow is present (e.g. big phone or tablet), it's assumed we have
567     * plenty of screen real estate and can show both.
568     */
569    private void setReplyOrReplyAllVisible() {
570        if (mIsDraft) {
571            setChildVisibility(GONE, mReplyButton, mReplyAllButton);
572            return;
573        } else if (mOverflowButton == null) {
574            setChildVisibility(VISIBLE, mReplyButton, mReplyAllButton);
575            return;
576        }
577
578        setChildVisibility(mDefaultReplyAll ? GONE : VISIBLE, mReplyButton);
579        setChildVisibility(mDefaultReplyAll ? VISIBLE : GONE, mReplyAllButton);
580    }
581
582    private static void setChildMarginRight(View childView, int marginRight) {
583        MarginLayoutParams mlp = (MarginLayoutParams) childView.getLayoutParams();
584        mlp.rightMargin = marginRight;
585        childView.setLayoutParams(mlp);
586    }
587
588    private void renderEmailList(int rowRes, int valueRes, String[] emails) {
589        if (emails == null || emails.length == 0) {
590            return;
591        }
592        String[] formattedEmails = new String[emails.length];
593        for (int i = 0; i < emails.length; i++) {
594            Address e = getAddress(emails[i]);
595            String name = e.getName();
596            String addr = e.getAddress();
597            if (name == null || name.length() == 0) {
598                formattedEmails[i] = addr;
599            } else {
600                formattedEmails[i] = getResources().getString(R.string.address_display_format,
601                        name, addr);
602            }
603        }
604        ((TextView) findViewById(valueRes)).setText(TextUtils.join("\n", formattedEmails));
605        findViewById(rowRes).setVisibility(VISIBLE);
606    }
607
608    @Override
609    public void setMarginBottom(int bottomMargin) {
610        MarginLayoutParams p = (MarginLayoutParams) getLayoutParams();
611        if (p.bottomMargin != bottomMargin) {
612            p.bottomMargin = bottomMargin;
613            setLayoutParams(p);
614        }
615    }
616
617    public void setMarginTop(int topMargin) {
618        MarginLayoutParams p = (MarginLayoutParams) getLayoutParams();
619        if (p.topMargin != topMargin) {
620            p.topMargin = topMargin;
621            setLayoutParams(p);
622        }
623    }
624
625    public void setTranslateY(int offsetY) {
626        if (mDrawTranslateY != offsetY) {
627            mDrawTranslateY = offsetY;
628            invalidate();
629        }
630    }
631
632    /**
633     * Utility class to build a list of recipient lists.
634     */
635    private static class RecipientListsBuilder {
636        private final Context mContext;
637        private final String mMe;
638        private final SpannableStringBuilder mBuilder = new SpannableStringBuilder();
639        private final CharSequence mComma;
640        private final Map<String, Address> mAddressCache;
641
642        int mRecipientCount = 0;
643        boolean mFirst = true;
644
645        public RecipientListsBuilder(Context context, String me,
646                Map<String, Address> addressCache) {
647            mContext = context;
648            mMe = me;
649            mComma = mContext.getText(R.string.enumeration_comma);
650            mAddressCache = addressCache;
651        }
652
653        public void append(String[] recipients, int headingRes) {
654            int addLimit = SUMMARY_MAX_RECIPIENTS - mRecipientCount;
655            CharSequence recipientList = getSummaryTextForHeading(headingRes, recipients, addLimit);
656            if (recipientList != null) {
657                // duplicate TextUtils.join() logic to minimize temporary
658                // allocations, and because we need to support spans
659                if (mFirst) {
660                    mFirst = false;
661                } else {
662                    mBuilder.append(RECIPIENT_HEADING_DELIMITER);
663                }
664                mBuilder.append(recipientList);
665                mRecipientCount += Math.min(addLimit, recipients.length);
666            }
667        }
668
669        private CharSequence getSummaryTextForHeading(int headingStrRes, String[] rawAddrs,
670                int maxToCopy) {
671            if (rawAddrs == null || rawAddrs.length == 0 || maxToCopy == 0) {
672                return null;
673            }
674
675            SpannableStringBuilder ssb = new SpannableStringBuilder(
676                    mContext.getString(headingStrRes));
677            ssb.setSpan(new StyleSpan(Typeface.BOLD), 0, ssb.length(),
678                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
679            ssb.append(' ');
680
681            final int len = Math.min(maxToCopy, rawAddrs.length);
682            boolean first = true;
683            for (int i = 0; i < len; i++) {
684                Address email = getAddress(mAddressCache, rawAddrs[i]);
685                String name = (mMe.equals(email.getAddress())) ? mContext.getString(R.string.me)
686                        : email.getSimplifiedName();
687
688                // duplicate TextUtils.join() logic to minimize temporary
689                // allocations, and because we need to support spans
690                if (first) {
691                    first = false;
692                } else {
693                    ssb.append(mComma);
694                }
695                ssb.append(name);
696            }
697
698            return ssb;
699        }
700
701        public CharSequence build() {
702            return mBuilder;
703        }
704    }
705
706    @VisibleForTesting
707    static CharSequence getRecipientSummaryText(Context context, String me, String[] to,
708            String[] cc, String[] bcc, Map<String, Address> addressCache) {
709
710        RecipientListsBuilder builder = new RecipientListsBuilder(context, me, addressCache);
711
712        builder.append(to, R.string.to_heading);
713        builder.append(cc, R.string.cc_heading);
714        builder.append(bcc, R.string.bcc_heading);
715
716        return builder.build();
717    }
718
719    @Override
720    public void updateContactInfo() {
721
722        mPresenceView.setImageDrawable(null);
723        mPresenceView.setVisibility(GONE);
724        if (mContactInfoSource == null || mSender == null) {
725            mPhotoView.setImageToDefault();
726            mPhotoView.setContentDescription(getResources().getString(
727                    R.string.contact_info_string_default));
728            return;
729        }
730
731        // Set the photo to either a found Bitmap or the default
732        // and ensure either the contact URI or email is set so the click
733        // handling works
734        String contentDesc = getResources().getString(R.string.contact_info_string,
735                !TextUtils.isEmpty(mSender.getName()) ? mSender.getName() : mSender.getAddress());
736        mPhotoView.setContentDescription(contentDesc);
737        boolean photoSet = false;
738        String email = mSender.getAddress();
739        ContactInfo info = mContactInfoSource.getContactInfo(email);
740        if (info != null) {
741            mPhotoView.assignContactUri(info.contactUri);
742            if (info.photo != null) {
743                mPhotoView.setImageBitmap(info.photo);
744                contentDesc = String.format(contentDesc, mSender.getName());
745                photoSet = true;
746            }
747            if (!mIsDraft && info.status != null) {
748                mPresenceView.setImageResource(ContactsContract.StatusUpdates
749                        .getPresenceIconResourceId(info.status));
750                mPresenceView.setVisibility(VISIBLE);
751            }
752        } else {
753            mPhotoView.assignContactFromEmail(email, true /* lazyLookup */);
754        }
755
756        if (!photoSet) {
757            mPhotoView.setImageToDefault();
758        }
759    }
760
761
762    @Override
763    public boolean onMenuItemClick(MenuItem item) {
764        mPopup.dismiss();
765        return onClick(null, item.getItemId());
766    }
767
768    @Override
769    public void onClick(View v) {
770        onClick(v, v.getId());
771    }
772
773    /**
774     * Handles clicks on either views or menu items. View parameter can be null
775     * for menu item clicks.
776     */
777    public boolean onClick(View v, int id) {
778        boolean handled = true;
779
780        switch (id) {
781            case R.id.reply:
782                ComposeActivity.reply(getContext(), mAccount, mMessage);
783                break;
784            case R.id.reply_all:
785                ComposeActivity.replyAll(getContext(), mAccount, mMessage);
786                break;
787            case R.id.forward:
788                ComposeActivity.forward(getContext(), mAccount, mMessage);
789                break;
790            case R.id.star: {
791                final boolean newValue = !v.isSelected();
792                v.setSelected(newValue);
793                mMessage.star(newValue, getQueryHandler(), 0 /* token */, null /* cookie */);
794                // TODO: propagate the change to the entry in conversation list
795                break;
796            }
797            case R.id.edit_draft:
798                ComposeActivity.editDraft(getContext(), mAccount, mMessage);
799                break;
800            case R.id.overflow: {
801                if (mPopup == null) {
802                    mPopup = new PopupMenu(getContext(), v);
803                    mPopup.getMenuInflater().inflate(R.menu.message_header_overflow_menu,
804                            mPopup.getMenu());
805                    mPopup.setOnMenuItemClickListener(this);
806                }
807                mPopup.getMenu().findItem(R.id.reply).setVisible(mDefaultReplyAll);
808                mPopup.getMenu().findItem(R.id.reply_all).setVisible(!mDefaultReplyAll);
809
810                mPopup.show();
811                break;
812            }
813            case R.id.details_collapsed_content:
814            case R.id.details_expanded_content:
815                toggleMessageDetails(v);
816                break;
817            case R.id.upper_header:
818                toggleExpanded();
819                break;
820            case R.id.show_pictures:
821                handleShowImagePromptClick(v);
822                break;
823            default:
824                LogUtils.i(LOG_TAG, "unrecognized header tap: %d", id);
825                handled = false;
826                break;
827        }
828        return handled;
829    }
830
831    public void toggleExpanded() {
832        if (mIsSnappy) {
833            // In addition to making the snappy header disappear, this will
834            // propagate the change to the normal header. It should only be
835            // possible to collapse an expanded snappy header; collapsed snappy
836            // headers should never exist.
837
838            // TODO: make this work right. the scroll position jumps and the
839            // snappy header doesn't re-appear bound to a subsequent message.
840            // mCallbacks.setMessageExpanded(mLocalMessageId, mServerMessageId,
841            // false);
842            // setVisibility(GONE);
843            // unbind();
844            return;
845        }
846
847        setExpanded(!isExpanded());
848
849        mSenderNameView.setText(getHeaderTitle());
850        mSenderEmailView.setText(getHeaderSubtitle());
851
852        updateChildVisibility();
853
854        // Force-measure the new header height so we can set the spacer size and
855        // reveal the message
856        // div in one pass. Force-measuring makes it unnecessary to set
857        // mSizeChanged.
858        int h = measureHeight();
859        mMessageHeaderItem.setHeight(h);
860        if (mCallbacks != null) {
861            mCallbacks.setMessageExpanded(mMessageHeaderItem, h);
862        }
863    }
864
865    private void toggleMessageDetails(View visibleDetailsView) {
866        final boolean detailsExpanded = (visibleDetailsView == mCollapsedDetailsView);
867        setMessageDetailsExpanded(detailsExpanded);
868        updateSpacerHeight();
869    }
870
871    private void setMessageDetailsExpanded(boolean expand) {
872        if (expand) {
873            showExpandedDetails();
874            hideCollapsedDetails();
875        } else {
876            hideExpandedDetails();
877            showCollapsedDetails();
878        }
879        if (mMessageHeaderItem != null) {
880            mMessageHeaderItem.detailsExpanded = expand;
881        }
882    }
883
884    public void setMessageDetailsVisibility(int vis) {
885        if (vis == GONE) {
886            hideCollapsedDetails();
887            hideExpandedDetails();
888            hideShowImagePrompt();
889        } else {
890            setMessageDetailsExpanded(mMessageHeaderItem.detailsExpanded);
891            if (mShowImagePrompt) {
892                showImagePrompt();
893            } else {
894                hideShowImagePrompt();
895            }
896        }
897        if (mBottomBorderView != null) {
898            mBottomBorderView.setVisibility(vis);
899        }
900    }
901
902    public void hideMessageDetails() {
903        setMessageDetailsVisibility(GONE);
904    }
905
906    @Override
907    public void setStarDisplay(boolean starred) {
908        if (mStarView.isSelected() != starred) {
909            mStarView.setSelected(starred);
910        }
911    }
912
913    private void hideCollapsedDetails() {
914        if (mCollapsedDetailsView != null) {
915            mCollapsedDetailsView.setVisibility(GONE);
916        }
917    }
918
919    private void hideExpandedDetails() {
920        if (mExpandedDetailsView != null) {
921            mExpandedDetailsView.setVisibility(GONE);
922        }
923    }
924
925    private void hideShowImagePrompt() {
926        if (mImagePromptView != null) {
927            mImagePromptView.setVisibility(GONE);
928        }
929    }
930
931    private void showImagePrompt() {
932        if (mImagePromptView == null) {
933            ViewGroup v = (ViewGroup) mInflater.inflate(R.layout.conversation_message_show_pics,
934                    this, false);
935            addView(v);
936            v.setOnClickListener(this);
937            v.setTag(SHOW_IMAGE_PROMPT_ONCE);
938
939            mImagePromptView = v;
940        }
941        mImagePromptView.setVisibility(VISIBLE);
942    }
943
944    private void handleShowImagePromptClick(View v) {
945        Integer state = (Integer) v.getTag();
946        if (state == null) {
947            return;
948        }
949        switch (state) {
950            case SHOW_IMAGE_PROMPT_ONCE:
951                if (mCallbacks != null) {
952                    mCallbacks.showExternalResources(mMessage);
953                }
954                ImageView descriptionViewIcon = (ImageView) v.findViewById(R.id.show_pictures_icon);
955                descriptionViewIcon.setContentDescription(getResources().getString(
956                        R.string.always_show_images));
957                TextView descriptionView = (TextView) v.findViewById(R.id.show_pictures_text);
958                descriptionView.setText(R.string.always_show_images);
959                v.setTag(SHOW_IMAGE_PROMPT_ALWAYS);
960                // the new text's line count may differ, so update the spacer height
961                updateSpacerHeight();
962                break;
963            case SHOW_IMAGE_PROMPT_ALWAYS:
964                mMessage.markAlwaysShowImages(getQueryHandler(), 0 /* token */, null /* cookie */);
965
966                mShowImagePrompt = false;
967                v.setTag(null);
968                v.setVisibility(GONE);
969                updateSpacerHeight();
970                Toast.makeText(getContext(), R.string.always_show_images_toast, Toast.LENGTH_SHORT)
971                        .show();
972                break;
973        }
974    }
975
976    private AsyncQueryHandler getQueryHandler() {
977        if (mQueryHandler == null) {
978            mQueryHandler = new AsyncQueryHandler(getContext().getContentResolver()) {};
979        }
980        return mQueryHandler;
981    }
982
983    /**
984     * Makes collapsed details visible. If necessary, will inflate details
985     * layout and render using saved-off state (senders, timestamp, etc).
986     */
987    private void showCollapsedDetails() {
988        if (mCollapsedDetailsView == null) {
989            mCollapsedDetailsView = (ViewGroup) mInflater.inflate(
990                    R.layout.conversation_message_details_header, this, false);
991            addView(mCollapsedDetailsView, indexOfChild(mUpperHeaderView) + 1);
992            mCollapsedDetailsView.setOnClickListener(this);
993        }
994        if (!mCollapsedDetailsValid) {
995            if (mMessageHeaderItem.recipientSummaryText == null) {
996                mMessageHeaderItem.recipientSummaryText = getRecipientSummaryText(getContext(),
997                        mAccount.name, mTo, mCc, mBcc, mAddressCache);
998            }
999            ((TextView) findViewById(R.id.recipients_summary))
1000                    .setText(mMessageHeaderItem.recipientSummaryText);
1001
1002            ((TextView) findViewById(R.id.date_summary)).setText(mTimestampShort);
1003
1004            mCollapsedDetailsValid = true;
1005        }
1006        mCollapsedDetailsView.setVisibility(VISIBLE);
1007    }
1008
1009    /**
1010     * Makes expanded details visible. If necessary, will inflate expanded
1011     * details layout and render using saved-off state (senders, timestamp,
1012     * etc).
1013     */
1014    private void showExpandedDetails() {
1015        // lazily create expanded details view
1016        if (mExpandedDetailsView == null) {
1017            View v = mInflater.inflate(R.layout.conversation_message_details_header_expanded,
1018                    this, false);
1019            addView(v, indexOfChild(mUpperHeaderView) + 1);
1020            v.setOnClickListener(this);
1021
1022            mExpandedDetailsView = (ViewGroup) v;
1023        }
1024        if (!mExpandedDetailsValid) {
1025            if (mMessageHeaderItem.timestampLong == null) {
1026                mMessageHeaderItem.timestampLong = mDateBuilder.formatLongDateTime(mTimestampMs);
1027            }
1028            ((TextView) findViewById(R.id.date_value)).setText(mMessageHeaderItem.timestampLong);
1029            renderEmailList(R.id.replyto_row, R.id.replyto_value, mReplyTo);
1030            renderEmailList(R.id.to_row, R.id.to_value, mTo);
1031            renderEmailList(R.id.cc_row, R.id.cc_value, mCc);
1032            renderEmailList(R.id.bcc_row, R.id.bcc_value, mBcc);
1033
1034            mExpandedDetailsValid = true;
1035        }
1036        mExpandedDetailsView.setVisibility(VISIBLE);
1037    }
1038
1039    /**
1040     * Returns a short plaintext snippet generated from the given HTML message
1041     * body. Collapses whitespace, ignores '&lt;' and '&gt;' characters and
1042     * everything in between, and truncates the snippet to no more than 100
1043     * characters.
1044     *
1045     * @return Short plaintext snippet
1046     */
1047    @VisibleForTesting
1048    static String makeSnippet(final String messageBody) {
1049        StringBuilder snippet = new StringBuilder(MAX_SNIPPET_LENGTH);
1050
1051        StringReader reader = new StringReader(messageBody);
1052        try {
1053            int c;
1054            while ((c = reader.read()) != -1 && snippet.length() < MAX_SNIPPET_LENGTH) {
1055                // Collapse whitespace.
1056                if (Character.isWhitespace(c)) {
1057                    snippet.append(' ');
1058                    do {
1059                        c = reader.read();
1060                    } while (Character.isWhitespace(c));
1061                    if (c == -1) {
1062                        break;
1063                    }
1064                }
1065
1066                if (c == '<') {
1067                    // Ignore everything up to and including the next '>'
1068                    // character.
1069                    while ((c = reader.read()) != -1) {
1070                        if (c == '>') {
1071                            break;
1072                        }
1073                    }
1074
1075                    // If we reached the end of the message body, exit.
1076                    if (c == -1) {
1077                        break;
1078                    }
1079                } else if (c == '&') {
1080                    // Read HTML entity.
1081                    StringBuilder sb = new StringBuilder();
1082
1083                    while ((c = reader.read()) != -1) {
1084                        if (c == ';') {
1085                            break;
1086                        }
1087                        sb.append((char) c);
1088                    }
1089
1090                    String entity = sb.toString();
1091                    if ("nbsp".equals(entity)) {
1092                        snippet.append(' ');
1093                    } else if ("lt".equals(entity)) {
1094                        snippet.append('<');
1095                    } else if ("gt".equals(entity)) {
1096                        snippet.append('>');
1097                    } else if ("amp".equals(entity)) {
1098                        snippet.append('&');
1099                    } else if ("quot".equals(entity)) {
1100                        snippet.append('"');
1101                    } else if ("apos".equals(entity) || "#39".equals(entity)) {
1102                        snippet.append('\'');
1103                    } else {
1104                        // Unknown entity; just append the literal string.
1105                        snippet.append('&').append(entity);
1106                        if (c == ';') {
1107                            snippet.append(';');
1108                        }
1109                    }
1110
1111                    // If we reached the end of the message body, exit.
1112                    if (c == -1) {
1113                        break;
1114                    }
1115                } else {
1116                    // The current character is a non-whitespace character that
1117                    // isn't inside some
1118                    // HTML tag and is not part of an HTML entity.
1119                    snippet.append((char) c);
1120                }
1121            }
1122        } catch (IOException e) {
1123            LogUtils.wtf(LOG_TAG, e, "Really? IOException while reading a freaking string?!? ");
1124        }
1125
1126        return snippet.toString();
1127    }
1128
1129    @Override
1130    public void dispatchDraw(Canvas canvas) {
1131        boolean transform = mIsSnappy && (mDrawTranslateY != 0);
1132        int saved = -1;
1133        if (transform) {
1134            saved = canvas.save();
1135            canvas.translate(0, mDrawTranslateY);
1136        }
1137        super.dispatchDraw(canvas);
1138        if (transform) {
1139            canvas.restoreToCount(saved);
1140        }
1141    }
1142
1143    @Override
1144    protected void onLayout(boolean changed, int l, int t, int r, int b) {
1145        Timer perf = new Timer();
1146        perf.start(LAYOUT_TAG);
1147        super.onLayout(changed, l, t, r, b);
1148        perf.pause(LAYOUT_TAG);
1149    }
1150
1151    @Override
1152    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1153        Timer t = new Timer();
1154        if (Timer.ENABLE_TIMER && !mPreMeasuring) {
1155            t.count("header measure id=" + mMessage.id);
1156            t.start(MEASURE_TAG);
1157        }
1158        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1159        if (!mPreMeasuring) {
1160            t.pause(MEASURE_TAG);
1161        }
1162    }
1163
1164}
1165