ConversationItemView.java revision 8d69d4e10a9a36ff790babb2f3a098a12d0dc732
1/*
2 * Copyright (C) 2012 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.mail.browse;
19
20import com.google.common.annotations.VisibleForTesting;
21
22import android.content.Context;
23import android.content.res.Resources;
24import android.database.Cursor;
25import android.graphics.Bitmap;
26import android.graphics.BitmapFactory;
27import android.graphics.Canvas;
28import android.graphics.Color;
29import android.graphics.Rect;
30import android.graphics.Typeface;
31import android.graphics.drawable.Drawable;
32import android.text.Layout.Alignment;
33import android.text.Spannable;
34import android.text.SpannableStringBuilder;
35import android.text.StaticLayout;
36import android.text.TextPaint;
37import android.text.TextUtils;
38import android.text.TextUtils.TruncateAt;
39import android.text.format.DateUtils;
40import android.text.style.CharacterStyle;
41import android.text.style.ForegroundColorSpan;
42import android.text.style.StyleSpan;
43import android.text.util.Rfc822Token;
44import android.text.util.Rfc822Tokenizer;
45import android.util.SparseArray;
46import android.view.MotionEvent;
47import android.view.View;
48
49import com.android.mail.R;
50import com.android.mail.browse.ConversationItemViewModel.SenderFragment;
51import com.android.mail.perf.Timer;
52import com.android.mail.providers.Address;
53import com.android.mail.providers.Conversation;
54import com.android.mail.providers.UIProvider.ConversationColumns;
55import com.android.mail.ui.ConversationSelectionSet;
56import com.android.mail.ui.ViewMode;
57import com.android.mail.utils.Utils;
58
59public class ConversationItemView extends View {
60    // Timer.
61    private static int sLayoutCount = 0;
62    private static Timer sTimer; // Create the sTimer here if you need to do perf analysis.
63    private static final int PERF_LAYOUT_ITERATIONS = 50;
64    private static final String PERF_TAG_LAYOUT = "CCHV.layout";
65    private static final String PERF_TAG_CALCULATE_TEXTS_BITMAPS = "CCHV.txtsbmps";
66    private static final String PERF_TAG_CALCULATE_SENDER_SUBJECT = "CCHV.sendersubj";
67    private static final String PERF_TAG_CALCULATE_LABELS = "CCHV.labels";
68    private static final String PERF_TAG_CALCULATE_COORDINATES = "CCHV.coordinates";
69
70    // Static bitmaps.
71    private static Bitmap CHECKMARK_OFF;
72    private static Bitmap CHECKMARK_ON;
73    private static Bitmap STAR_OFF;
74    private static Bitmap STAR_ON;
75    private static Bitmap ATTACHMENT;
76    private static Bitmap ONLY_TO_ME;
77    private static Bitmap TO_ME_AND_OTHERS;
78    private static Bitmap IMPORTANT_ONLY_TO_ME;
79    private static Bitmap IMPORTANT_TO_ME_AND_OTHERS;
80    private static Bitmap IMPORTANT_TO_OTHERS;
81    private static Bitmap DATE_BACKGROUND;
82
83    // Static colors.
84    private static int DEFAULT_TEXT_COLOR;
85    private static int ACTIVATED_TEXT_COLOR;
86    private static int LIGHT_TEXT_COLOR;
87    private static int DRAFT_TEXT_COLOR;
88    private static int SUBJECT_TEXT_COLOR_READ;
89    private static int SUBJECT_TEXT_COLOR_UNREAD;
90    private static int SNIPPET_TEXT_COLOR_READ;
91    private static int SNIPPET_TEXT_COLOR_UNREAD;
92    private static int SENDERS_TEXT_COLOR_READ;
93    private static int SENDERS_TEXT_COLOR_UNREAD;
94    private static int DATE_TEXT_COLOR_READ;
95    private static int DATE_TEXT_COLOR_UNREAD;
96    private static int DATE_BACKGROUND_PADDING_LEFT;
97    private static int TOUCH_SLOP;
98    private static int sDateBackgroundHeight;
99    private static int sStandardScaledDimen;
100    private static CharacterStyle sLightTextStyle;
101    private static CharacterStyle sNormalTextStyle;
102
103    // Static paints.
104    private static TextPaint sPaint = new TextPaint();
105    private static TextPaint sLabelsPaint = new TextPaint();
106
107    // Backgrounds for different states.
108    private final SparseArray<Drawable> mBackgrounds = new SparseArray<Drawable>();
109
110    // Dimensions and coordinates.
111    private int mViewWidth = -1;
112    private int mMode = -1;
113    private int mDateX;
114    private int mPaperclipX;
115    private int mLabelsXEnd;
116    private int mSendersWidth;
117
118    /** Whether we're running under test mode. */
119    private boolean mTesting = false;
120
121    @VisibleForTesting
122    ConversationItemViewCoordinates mCoordinates;
123
124    private final Context mContext;
125
126    private String mAccount;
127    private ConversationItemViewModel mHeader;
128    private ViewMode mViewMode;
129    private boolean mDownEvent;
130    private boolean mChecked;
131    private static int sFadedColor = -1;
132    private static int sFadedActivatedColor = -1;
133    private ConversationSelectionSet mSelectedConversationSet;
134
135    static {
136        sPaint.setAntiAlias(true);
137        sLabelsPaint.setAntiAlias(true);
138    }
139
140    /**
141     * This handler will be called when user toggle a star in a conversation
142     * header view. It can be used to update the state of other views to ensure
143     * UI consistency.
144     */
145    public static interface StarHandler {
146        public void toggleStar(boolean toggleOn, long conversationId, long maxMessageId);
147    }
148
149    public ConversationItemView(Context context, String account) {
150        super(context);
151        mContext = context.getApplicationContext();
152        mAccount = account;
153        Resources res = mContext.getResources();
154
155        if (CHECKMARK_OFF == null) {
156            // Initialize static bitmaps.
157            CHECKMARK_OFF = BitmapFactory.decodeResource(res,
158                    R.drawable.btn_check_off_normal_holo_light);
159            CHECKMARK_ON = BitmapFactory.decodeResource(res,
160                    R.drawable.btn_check_on_normal_holo_light);
161            STAR_OFF = BitmapFactory.decodeResource(res,
162                    R.drawable.btn_star_off_normal_email_holo_light);
163            STAR_ON = BitmapFactory.decodeResource(res,
164                    R.drawable.btn_star_on_normal_email_holo_light);
165            ONLY_TO_ME = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_double);
166            TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_single);
167            IMPORTANT_ONLY_TO_ME = BitmapFactory.decodeResource(res,
168                    R.drawable.ic_email_caret_double_important_unread);
169            IMPORTANT_TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res,
170                    R.drawable.ic_email_caret_single_important_unread);
171            IMPORTANT_TO_OTHERS = BitmapFactory.decodeResource(res,
172                    R.drawable.ic_email_caret_none_important_unread);
173            ATTACHMENT = BitmapFactory.decodeResource(res, R.drawable.ic_attachment_holo_light);
174            DATE_BACKGROUND = BitmapFactory.decodeResource(res, R.drawable.label_bg_holo_light);
175
176            // Initialize colors.
177            DEFAULT_TEXT_COLOR = res.getColor(R.color.default_text_color);
178            ACTIVATED_TEXT_COLOR = res.getColor(android.R.color.white);
179            LIGHT_TEXT_COLOR = res.getColor(R.color.light_text_color);
180            DRAFT_TEXT_COLOR = res.getColor(R.color.drafts);
181            SUBJECT_TEXT_COLOR_READ = res.getColor(R.color.subject_text_color_read);
182            SUBJECT_TEXT_COLOR_UNREAD = res.getColor(R.color.subject_text_color_unread);
183            SNIPPET_TEXT_COLOR_READ = res.getColor(R.color.snippet_text_color_read);
184            SNIPPET_TEXT_COLOR_UNREAD = res.getColor(R.color.snippet_text_color_unread);
185            SENDERS_TEXT_COLOR_READ = res.getColor(R.color.senders_text_color_read);
186            SENDERS_TEXT_COLOR_UNREAD = res.getColor(R.color.senders_text_color_unread);
187            DATE_TEXT_COLOR_READ = res.getColor(R.color.date_text_color_read);
188            DATE_TEXT_COLOR_UNREAD = res.getColor(R.color.date_text_color_unread);
189            DATE_BACKGROUND_PADDING_LEFT = res
190                    .getDimensionPixelSize(R.dimen.date_background_padding_left);
191            TOUCH_SLOP = res.getDimensionPixelSize(R.dimen.touch_slop);
192            sDateBackgroundHeight = res.getDimensionPixelSize(R.dimen.date_background_height);
193            sStandardScaledDimen = res.getDimensionPixelSize(R.dimen.standard_scaled_dimen);
194
195            // Initialize static color.
196            sNormalTextStyle = new StyleSpan(Typeface.NORMAL);
197            sLightTextStyle = new ForegroundColorSpan(LIGHT_TEXT_COLOR);
198        }
199    }
200
201    /**
202     * Bind this view to the content of the cursor and request layout.
203     */
204    public void bind(ConversationItemViewModel model, StarHandler starHandler, String account,
205            CharSequence displayedLabel, ViewMode viewMode, ConversationSelectionSet set) {
206        mAccount = account;
207        mViewMode = viewMode;
208        mHeader = model;
209        mSelectedConversationSet = set;
210        setContentDescription(mHeader.getContentDescription(mContext));
211        requestLayout();
212    }
213
214    public void bind(Cursor cursor, StarHandler starHandler, String account,
215            CharSequence displayedLabel, ViewMode viewMode, ConversationSelectionSet set) {
216        mAccount = account;
217        mViewMode = viewMode;
218        mHeader = ConversationItemViewModel.forCursor(account, cursor);
219        mSelectedConversationSet = set;
220        setContentDescription(mHeader.getContentDescription(mContext));
221        requestLayout();
222    }
223
224    public Conversation getConversation() {
225        return mHeader.conversation;
226    }
227
228    /**
229     * Sets the mode. Only used for testing.
230     */
231    @VisibleForTesting
232    void setMode(int mode) {
233        mMode = mode;
234        mTesting = true;
235    }
236
237    private static void startTimer(String tag) {
238        if (sTimer != null) {
239            sTimer.start(tag);
240        }
241    }
242
243    private static void pauseTimer(String tag) {
244        if (sTimer != null) {
245            sTimer.pause(tag);
246        }
247    }
248
249    @Override
250    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
251        startTimer(PERF_TAG_LAYOUT);
252
253        super.onLayout(changed, left, top, right, bottom);
254
255        int width = right - left;
256        if (width != mViewWidth) {
257            mViewWidth = width;
258            if (!mTesting) {
259                mMode = ConversationItemViewCoordinates.getMode(mContext, mViewMode);
260            }
261        }
262        mHeader.viewWidth = mViewWidth;
263        Resources res = getResources();
264        mHeader.standardScaledDimen = res.getDimensionPixelOffset(R.dimen.standard_scaled_dimen);
265        if (mHeader.standardScaledDimen != sStandardScaledDimen) {
266            // Large Text has been toggle on/off. Update the static dimens.
267            sStandardScaledDimen = mHeader.standardScaledDimen;
268            ConversationItemViewCoordinates.refreshConversationHeights(mContext);
269            sDateBackgroundHeight = res.getDimensionPixelSize(R.dimen.date_background_height);
270        }
271        mCoordinates = ConversationItemViewCoordinates.forWidth(mContext, mViewWidth, mMode,
272                mHeader.standardScaledDimen);
273        calculateTextsAndBitmaps();
274        calculateCoordinates();
275        mHeader.validate(mContext);
276
277        pauseTimer(PERF_TAG_LAYOUT);
278        if (sTimer != null && ++sLayoutCount >= PERF_LAYOUT_ITERATIONS) {
279            sTimer.dumpResults();
280            sTimer = new Timer();
281            sLayoutCount = 0;
282        }
283    }
284
285    @Override
286    public void setBackgroundResource(int resourceId) {
287        Drawable drawable = mBackgrounds.get(resourceId);
288        if (drawable == null) {
289            drawable = getResources().getDrawable(resourceId);
290            mBackgrounds.put(resourceId, drawable);
291        }
292        if (getBackground() != drawable) {
293            super.setBackgroundDrawable(drawable);
294        }
295    }
296
297    private void calculateTextsAndBitmaps() {
298        startTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
299        mChecked = mSelectedConversationSet != null
300                && mSelectedConversationSet.containsKey(mHeader.conversation.id);
301        // Update font color.
302        int fontColor = getFontColor(DEFAULT_TEXT_COLOR);
303        boolean fontChanged = false;
304        if (mHeader.fontColor != fontColor) {
305            fontChanged = true;
306            mHeader.fontColor = fontColor;
307        }
308
309        boolean isUnread = mHeader.unread;
310
311        final boolean checkboxEnabled = true;
312        if (mHeader.checkboxVisible != checkboxEnabled) {
313            mHeader.checkboxVisible = checkboxEnabled;
314        }
315
316        // Update background.
317        updateBackground(isUnread);
318
319        if (mHeader.isLayoutValid(mContext)) {
320            // Relayout subject if font color has changed.
321            if (fontChanged) {
322                createSubjectSpans(isUnread);
323                layoutSubject();
324            }
325            pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
326            return;
327        }
328
329        // Initialize label displayer.
330        startTimer(PERF_TAG_CALCULATE_LABELS);
331
332        pauseTimer(PERF_TAG_CALCULATE_LABELS);
333
334        // Star.
335        mHeader.starBitmap = mHeader.starred ? STAR_ON : STAR_OFF;
336
337        // Date.
338        mHeader.dateText = DateUtils.getRelativeTimeSpanString(mContext,
339                mHeader.conversation.dateMs).toString();
340
341        // Paper clip icon.
342        mHeader.paperclip = null;
343        if (mHeader.conversation.hasAttachments) {
344            mHeader.paperclip = ATTACHMENT;
345        }
346
347        // Personal level.
348        mHeader.personalLevelBitmap = null;
349
350        startTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT);
351
352        // Subject.
353        createSubjectSpans(isUnread);
354
355        // Parse senders fragments.
356        parseSendersFragments(isUnread);
357
358        pauseTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT);
359        pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
360    }
361
362    private void createSubjectSpans(boolean isUnread) {
363        final String subject = filterTag(mHeader.conversation.subject);
364        final String snippet = mHeader.conversation.snippet;
365        int subjectColor = isUnread ? SUBJECT_TEXT_COLOR_UNREAD : SUBJECT_TEXT_COLOR_READ;
366        int snippetColor = isUnread ? SNIPPET_TEXT_COLOR_UNREAD : SNIPPET_TEXT_COLOR_READ;
367        mHeader.subjectText = new SpannableStringBuilder(mContext.getString(
368                R.string.subject_and_snippet, subject, snippet));
369        if (isUnread) {
370            mHeader.subjectText.setSpan(new StyleSpan(Typeface.BOLD), 0, subject.length(),
371                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
372        }
373        int fontColor = getFontColor(subjectColor);
374        mHeader.subjectText.setSpan(new ForegroundColorSpan(fontColor), 0,
375                subject.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
376        fontColor = getFontColor(snippetColor);
377        mHeader.subjectText.setSpan(new ForegroundColorSpan(fontColor), subject.length() + 1,
378                mHeader.subjectText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
379    }
380
381    private int getFontColor(int defaultColor) {
382        return isActivated() && mViewMode.isTwoPane() ? ACTIVATED_TEXT_COLOR
383                : defaultColor;
384    }
385
386    private void layoutSubject() {
387        sPaint.setTextSize(mCoordinates.subjectFontSize);
388        sPaint.setColor(mHeader.fontColor);
389        mHeader.subjectLayout = new StaticLayout(mHeader.subjectText, sPaint,
390                mCoordinates.subjectWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
391        if (mCoordinates.subjectLineCount < mHeader.subjectLayout.getLineCount()) {
392            int end = mHeader.subjectLayout.getLineEnd(mCoordinates.subjectLineCount - 1);
393            mHeader.subjectLayout = new StaticLayout(mHeader.subjectText.subSequence(0, end),
394                    sPaint, mCoordinates.subjectWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
395        }
396    }
397
398    /**
399     * Parses senders text into small fragments.
400     */
401    private void parseSendersFragments(boolean isUnread) {
402        if (TextUtils.isEmpty(mHeader.conversation.senders)) {
403            return;
404        }
405        mHeader.sendersText = formatSenders(mHeader.conversation.senders);
406        mHeader.addSenderFragment(0, mHeader.sendersText.length(), sNormalTextStyle, true);
407    }
408
409    private String formatSenders(String sendersString) {
410        String[] senders = TextUtils.split(sendersString, Address.ADDRESS_DELIMETER);
411        String[] namesOnly = new String[senders.length];
412        Rfc822Token[] senderTokens;
413        String display;
414        for (int i = 0; i < senders.length; i++) {
415            senderTokens = Rfc822Tokenizer.tokenize(senders[i]);
416            if (senderTokens != null && senderTokens.length > 0) {
417                display = senderTokens[0].getName();
418                if (TextUtils.isEmpty(display)) {
419                    display = senderTokens[0].getAddress();
420                }
421                namesOnly[i] = display;
422            }
423        }
424        return TextUtils.join(Address.ADDRESS_DELIMETER + " ", namesOnly);
425    }
426
427    private boolean canFitFragment(int width, int line, int fixedWidth) {
428        if (line == mCoordinates.sendersLineCount) {
429            return width + fixedWidth <= mSendersWidth;
430        } else {
431            return width <= mSendersWidth;
432        }
433    }
434
435    private void calculateCoordinates() {
436        startTimer(PERF_TAG_CALCULATE_COORDINATES);
437
438        sPaint.setTextSize(mCoordinates.dateFontSize);
439        sPaint.setTypeface(Typeface.DEFAULT);
440        mDateX = mCoordinates.dateXEnd - (int) sPaint.measureText(mHeader.dateText);
441
442        mPaperclipX = mDateX - ATTACHMENT.getWidth();
443
444        int cellWidth = mContext.getResources().getDimensionPixelSize(R.dimen.label_cell_width);
445
446        if (ConversationItemViewCoordinates.displayLabelsAboveDate(mMode)) {
447            mLabelsXEnd = mCoordinates.dateXEnd;
448            mSendersWidth = mCoordinates.sendersWidth;
449        } else {
450            if (mHeader.paperclip != null) {
451                mLabelsXEnd = mPaperclipX;
452            } else {
453                mLabelsXEnd = mDateX - cellWidth / 2;
454            }
455            mSendersWidth = mLabelsXEnd - mCoordinates.sendersX - 2 * cellWidth;
456        }
457
458        if (mHeader.isLayoutValid(mContext)) {
459            pauseTimer(PERF_TAG_CALCULATE_COORDINATES);
460            return;
461        }
462
463        // Layout subject.
464        layoutSubject();
465
466        // First pass to calculate width of each fragment.
467        int totalWidth = 0;
468        int fixedWidth = 0;
469        sPaint.setTextSize(mCoordinates.sendersFontSize);
470        sPaint.setTypeface(Typeface.DEFAULT);
471        for (SenderFragment senderFragment : mHeader.senderFragments) {
472            CharacterStyle style = senderFragment.style;
473            int start = senderFragment.start;
474            int end = senderFragment.end;
475            style.updateDrawState(sPaint);
476            senderFragment.width = (int) sPaint.measureText(mHeader.sendersText, start, end);
477            boolean isFixed = senderFragment.isFixed;
478            if (isFixed) {
479                fixedWidth += senderFragment.width;
480            }
481            totalWidth += senderFragment.width;
482        }
483
484        // Second pass to layout each fragment.
485        int sendersY = mCoordinates.sendersY - mCoordinates.sendersAscent;
486        if (!ConversationItemViewCoordinates.displaySendersInline(mMode)) {
487            sendersY += totalWidth <= mSendersWidth ? mCoordinates.sendersLineHeight / 2 : 0;
488        }
489        totalWidth = 0;
490        int currentLine = 1;
491        boolean ellipsize = false;
492        for (SenderFragment senderFragment : mHeader.senderFragments) {
493            CharacterStyle style = senderFragment.style;
494            int start = senderFragment.start;
495            int end = senderFragment.end;
496            int width = senderFragment.width;
497            boolean isFixed = senderFragment.isFixed;
498            style.updateDrawState(sPaint);
499
500            // No more width available, we'll only show fixed fragments.
501            if (ellipsize && !isFixed) {
502                senderFragment.shouldDisplay = false;
503                continue;
504            }
505
506            // New line and ellipsize text if needed.
507            senderFragment.ellipsizedText = null;
508            if (isFixed) {
509                fixedWidth -= width;
510            }
511            if (!canFitFragment(totalWidth + width, currentLine, fixedWidth)) {
512                // The text is too long, new line won't help. We have to
513                // ellipsize text.
514                if (totalWidth == 0) {
515                    ellipsize = true;
516                } else {
517                    // New line.
518                    if (currentLine < mCoordinates.sendersLineCount) {
519                        currentLine++;
520                        sendersY += mCoordinates.sendersLineHeight;
521                        totalWidth = 0;
522                        // The text is still too long, we have to ellipsize
523                        // text.
524                        if (totalWidth + width > mSendersWidth) {
525                            ellipsize = true;
526                        }
527                    } else {
528                        ellipsize = true;
529                    }
530                }
531
532                if (ellipsize) {
533                    width = mSendersWidth - totalWidth;
534                    // No more new line, we have to reserve width for fixed
535                    // fragments.
536                    if (currentLine == mCoordinates.sendersLineCount) {
537                        width -= fixedWidth;
538                    }
539                    senderFragment.ellipsizedText = TextUtils.ellipsize(
540                            mHeader.sendersText.substring(start, end), sPaint, width,
541                            TruncateAt.END).toString();
542                    width = (int) sPaint.measureText(senderFragment.ellipsizedText);
543                }
544            }
545            senderFragment.x = mCoordinates.sendersX + totalWidth;
546            senderFragment.y = sendersY;
547            senderFragment.shouldDisplay = true;
548            totalWidth += width;
549        }
550
551        pauseTimer(PERF_TAG_CALCULATE_COORDINATES);
552    }
553
554    /**
555     * If the subject contains the tag of a mailing-list (text surrounded with
556     * []), return the subject with that tag ellipsized, e.g.
557     * "[android-gmail-team] Hello" -> "[andr...] Hello"
558     */
559    private String filterTag(String subject) {
560        String result = subject;
561        String formatString = getContext().getResources().getString(R.string.filtered_tag);
562        if (!TextUtils.isEmpty(subject) && subject.charAt(0) == '[') {
563            int end = subject.indexOf(']');
564            if (end > 0) {
565                String tag = subject.substring(1, end);
566                result = String.format(formatString, Utils.ellipsize(tag, 7),
567                        subject.substring(end + 1));
568            }
569        }
570        return result;
571    }
572
573    @Override
574    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
575        int width = measureWidth(widthMeasureSpec);
576        int height = measureHeight(heightMeasureSpec,
577                ConversationItemViewCoordinates.getMode(mContext, mViewMode));
578        setMeasuredDimension(width, height);
579    }
580
581    /**
582     * Determine the width of this view.
583     *
584     * @param measureSpec A measureSpec packed into an int
585     * @return The width of the view, honoring constraints from measureSpec
586     */
587    private int measureWidth(int measureSpec) {
588        int result = 0;
589        int specMode = MeasureSpec.getMode(measureSpec);
590        int specSize = MeasureSpec.getSize(measureSpec);
591
592        if (specMode == MeasureSpec.EXACTLY) {
593            // We were told how big to be
594            result = specSize;
595        } else {
596            // Measure the text
597            result = mViewWidth;
598            if (specMode == MeasureSpec.AT_MOST) {
599                // Respect AT_MOST value if that was what is called for by
600                // measureSpec
601                result = Math.min(result, specSize);
602            }
603        }
604        return result;
605    }
606
607    /**
608     * Determine the height of this view.
609     *
610     * @param measureSpec A measureSpec packed into an int
611     * @param mode The current mode of this view
612     * @return The height of the view, honoring constraints from measureSpec
613     */
614    private int measureHeight(int measureSpec, int mode) {
615        int result = 0;
616        int specMode = MeasureSpec.getMode(measureSpec);
617        int specSize = MeasureSpec.getSize(measureSpec);
618
619        if (specMode == MeasureSpec.EXACTLY) {
620            // We were told how big to be
621            result = specSize;
622        } else {
623            // Measure the text
624            result = ConversationItemViewCoordinates.getHeight(mContext, mode);
625            if (specMode == MeasureSpec.AT_MOST) {
626                // Respect AT_MOST value if that was what is called for by
627                // measureSpec
628                result = Math.min(result, specSize);
629            }
630        }
631        return result;
632    }
633
634    @Override
635    protected void onDraw(Canvas canvas) {
636        // Check mark.
637        if (mHeader.checkboxVisible) {
638            Bitmap checkmark = mChecked ? CHECKMARK_ON : CHECKMARK_OFF;
639            canvas.drawBitmap(checkmark, mCoordinates.checkmarkX, mCoordinates.checkmarkY, sPaint);
640        }
641
642        // Personal Level.
643        if (mHeader.personalLevelBitmap != null) {
644            canvas.drawBitmap(mHeader.personalLevelBitmap, mCoordinates.personalLevelX,
645                    mCoordinates.personalLevelY, sPaint);
646        }
647
648        // Senders.
649        sPaint.setTextSize(mCoordinates.sendersFontSize);
650        sPaint.setTypeface(Typeface.DEFAULT);
651        boolean isUnread = mHeader.unread;
652        int sendersColor = getFontColor(isUnread ? SENDERS_TEXT_COLOR_UNREAD
653                : SENDERS_TEXT_COLOR_READ);
654        sPaint.setColor(sendersColor);
655        for (SenderFragment fragment : mHeader.senderFragments) {
656            if (fragment.shouldDisplay) {
657                sPaint.setTypeface(Typeface.DEFAULT);
658                fragment.style.updateDrawState(sPaint);
659                if (fragment.ellipsizedText != null) {
660                    canvas.drawText(fragment.ellipsizedText, fragment.x, fragment.y, sPaint);
661                } else {
662                    canvas.drawText(mHeader.sendersText, fragment.start, fragment.end, fragment.x,
663                            fragment.y, sPaint);
664                }
665            }
666        }
667
668        // Subject.
669        sPaint.setTextSize(mCoordinates.subjectFontSize);
670        sPaint.setTypeface(Typeface.DEFAULT);
671        sPaint.setColor(mHeader.fontColor);
672        canvas.save();
673        canvas.translate(mCoordinates.subjectX,
674                mCoordinates.subjectY + mHeader.subjectLayout.getTopPadding());
675        mHeader.subjectLayout.draw(canvas);
676        canvas.restore();
677
678        // Date background: shown when there is an attachment or a visible
679        // label.
680        if (!isActivated()
681                && mHeader.conversation.hasAttachments
682                && ConversationItemViewCoordinates.showAttachmentBackground(mMode)) {
683            mHeader.dateBackground = DATE_BACKGROUND;
684            int leftOffset = (mHeader.conversation.hasAttachments ? mPaperclipX : mDateX)
685                    - DATE_BACKGROUND_PADDING_LEFT;
686            int top = mCoordinates.labelsY;
687            Rect src = new Rect(0, 0, mHeader.dateBackground.getWidth(), mHeader.dateBackground
688                    .getHeight());
689            Rect dst = new Rect(leftOffset, top, mViewWidth, top + sDateBackgroundHeight);
690            canvas.drawBitmap(mHeader.dateBackground, src, dst, sPaint);
691        } else {
692            mHeader.dateBackground = null;
693        }
694
695        // Date.
696        sPaint.setTextSize(mCoordinates.dateFontSize);
697        sPaint.setTypeface(Typeface.DEFAULT);
698        sPaint.setColor(isUnread ? DATE_TEXT_COLOR_UNREAD : DATE_TEXT_COLOR_READ);
699        drawText(canvas, mHeader.dateText, mDateX, mCoordinates.dateY - mCoordinates.dateAscent,
700                sPaint);
701
702        // Paper clip icon.
703        if (mHeader.paperclip != null) {
704            canvas.drawBitmap(mHeader.paperclip, mPaperclipX, mCoordinates.paperclipY, sPaint);
705        }
706
707        if (mHeader.faded) {
708            int fadedColor = -1;
709            if (sFadedActivatedColor == -1) {
710                sFadedActivatedColor = mContext.getResources().getColor(
711                        R.color.faded_activated_conversation_header);
712            }
713            fadedColor = sFadedActivatedColor;
714            int restoreState = canvas.save();
715            Rect bounds = canvas.getClipBounds();
716            canvas.clipRect(bounds.left, bounds.top, bounds.right
717                    - mContext.getResources().getDimensionPixelSize(R.dimen.triangle_width),
718                    bounds.bottom);
719            canvas.drawARGB(Color.alpha(fadedColor), Color.red(fadedColor),
720                    Color.green(fadedColor), Color.blue(fadedColor));
721            canvas.restoreToCount(restoreState);
722        }
723
724        // Star.
725        canvas.drawBitmap(mHeader.starBitmap, mCoordinates.starX, mCoordinates.starY, sPaint);
726    }
727
728    private void drawText(Canvas canvas, CharSequence s, int x, int y, TextPaint paint) {
729        canvas.drawText(s, 0, s.length(), x, y, paint);
730    }
731
732    private void updateBackground(boolean isUnread) {
733        if (isUnread) {
734            if (mViewMode.isTwoPane() && mViewMode.isConversationListMode()) {
735                if (mChecked) {
736                    setBackgroundResource(R.drawable.list_conversation_wide_unread_selected_holo);
737                } else {
738                    setBackgroundResource(R.drawable.conversation_wide_unread_selector);
739                }
740            } else {
741                if (mChecked) {
742                    setCheckedActivatedBackground();
743                } else {
744                    setBackgroundResource(R.drawable.conversation_unread_selector);
745                }
746            }
747        } else {
748            if (mViewMode.isTwoPane() && mViewMode.isConversationListMode()) {
749                if (mChecked) {
750                    setBackgroundResource(R.drawable.list_conversation_wide_read_selected_holo);
751                } else {
752                    setBackgroundResource(R.drawable.conversation_wide_read_selector);
753                }
754            } else {
755                if (mChecked) {
756                    setCheckedActivatedBackground();
757                } else {
758                    setBackgroundResource(R.drawable.conversation_read_selector);
759                }
760            }
761        }
762    }
763
764    private void setCheckedActivatedBackground() {
765        if (isActivated() && mViewMode.isTwoPane()) {
766            setBackgroundResource(R.drawable.list_arrow_selected_holo);
767        } else {
768            setBackgroundResource(R.drawable.list_selected_holo);
769        }
770    }
771
772    /**
773     * Toggle the check mark on this view and update the conversation
774     */
775    public void toggleCheckMark() {
776        mChecked = !mChecked;
777        mSelectedConversationSet.toggle(mHeader.conversation);
778        // We update the background after the checked state has changed now that
779        // we have a selected background asset. Setting the background usually
780        // waits for a layout pass, but we don't need a full layout, just an
781        // update to the background.
782        requestLayout();
783    }
784
785    /**
786     * Toggle the star on this view and update the conversation.
787     */
788    private void toggleStar() {
789        mHeader.starred = !mHeader.starred;
790        mHeader.starBitmap = mHeader.starred ? STAR_ON : STAR_OFF;
791        postInvalidate(mCoordinates.starX, mCoordinates.starY, mCoordinates.starX
792                + mHeader.starBitmap.getWidth(),
793                mCoordinates.starY + mHeader.starBitmap.getHeight());
794        // Generalize this...
795        mHeader.conversation.updateBoolean(mContext, ConversationColumns.STARRED, mHeader.starred);
796    }
797
798    private boolean touchCheckmark(float x, float y) {
799        // Everything before senders and include a touch slop.
800        return mHeader.checkboxVisible && x < mCoordinates.sendersX + TOUCH_SLOP;
801    }
802
803    private boolean touchStar(float x, float y) {
804        // Everything after the star and include a touch slop.
805        return x > mCoordinates.starX - TOUCH_SLOP;
806    }
807
808    @Override
809    public boolean onTouchEvent(MotionEvent event) {
810        boolean handled = false;
811
812        int x = (int) event.getX();
813        int y = (int) event.getY();
814        switch (event.getAction()) {
815            case MotionEvent.ACTION_DOWN:
816                mDownEvent = true;
817                if (touchCheckmark(x, y) || touchStar(x, y)) {
818                    handled = true;
819                }
820                break;
821
822            case MotionEvent.ACTION_CANCEL:
823                mDownEvent = false;
824                break;
825
826            case MotionEvent.ACTION_UP:
827                if (mDownEvent) {
828                    if (touchCheckmark(x, y)) {
829                        // Touch on the check mark
830                        toggleCheckMark();
831                    } else if (touchStar(x, y)) {
832                        // Touch on the star
833                        toggleStar();
834                    }
835                    handled = true;
836                }
837                break;
838        }
839
840        if (!handled) {
841            handled = super.onTouchEvent(event);
842        }
843
844        return handled;
845    }
846}
847