ConversationItemView.java revision 4584a0d83e160444f931cb565185a2eea39b1683
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.LinearGradient;
30import android.graphics.Paint;
31import android.graphics.Rect;
32import android.graphics.Shader;
33import android.graphics.Typeface;
34import android.graphics.drawable.Drawable;
35import android.text.Layout.Alignment;
36import android.text.Spannable;
37import android.text.SpannableStringBuilder;
38import android.text.StaticLayout;
39import android.text.TextPaint;
40import android.text.TextUtils;
41import android.text.TextUtils.TruncateAt;
42import android.text.format.DateUtils;
43import android.text.style.CharacterStyle;
44import android.text.style.ForegroundColorSpan;
45import android.text.style.StyleSpan;
46import android.text.util.Rfc822Token;
47import android.text.util.Rfc822Tokenizer;
48import android.util.SparseArray;
49import android.view.MotionEvent;
50import android.view.View;
51import android.widget.ListView;
52
53import com.android.mail.R;
54import com.android.mail.browse.ConversationItemViewModel.SenderFragment;
55import com.android.mail.perf.Timer;
56import com.android.mail.providers.Address;
57import com.android.mail.providers.Conversation;
58import com.android.mail.providers.Folder;
59import com.android.mail.providers.UIProvider.ConversationColumns;
60import com.android.mail.ui.ConversationSelectionSet;
61import com.android.mail.ui.FolderDisplayer;
62import com.android.mail.ui.ViewMode;
63import com.android.mail.utils.Utils;
64
65import java.util.Map;
66
67public class ConversationItemView extends View {
68    // Timer.
69    private static int sLayoutCount = 0;
70    private static Timer sTimer; // Create the sTimer here if you need to do perf analysis.
71    private static final int PERF_LAYOUT_ITERATIONS = 50;
72    private static final String PERF_TAG_LAYOUT = "CCHV.layout";
73    private static final String PERF_TAG_CALCULATE_TEXTS_BITMAPS = "CCHV.txtsbmps";
74    private static final String PERF_TAG_CALCULATE_SENDER_SUBJECT = "CCHV.sendersubj";
75    private static final String PERF_TAG_CALCULATE_FOLDERS = "CCHV.folders";
76    private static final String PERF_TAG_CALCULATE_COORDINATES = "CCHV.coordinates";
77
78    // Static bitmaps.
79    private static Bitmap CHECKMARK_OFF;
80    private static Bitmap CHECKMARK_ON;
81    private static Bitmap STAR_OFF;
82    private static Bitmap STAR_ON;
83    private static Bitmap ATTACHMENT;
84    private static Bitmap ONLY_TO_ME;
85    private static Bitmap TO_ME_AND_OTHERS;
86    private static Bitmap IMPORTANT_ONLY_TO_ME;
87    private static Bitmap IMPORTANT_TO_ME_AND_OTHERS;
88    private static Bitmap IMPORTANT_TO_OTHERS;
89    private static Bitmap DATE_BACKGROUND;
90
91    // Static colors.
92    private static int DEFAULT_TEXT_COLOR;
93    private static int ACTIVATED_TEXT_COLOR;
94    private static int LIGHT_TEXT_COLOR;
95    private static int DRAFT_TEXT_COLOR;
96    private static int SUBJECT_TEXT_COLOR_READ;
97    private static int SUBJECT_TEXT_COLOR_UNREAD;
98    private static int SNIPPET_TEXT_COLOR_READ;
99    private static int SNIPPET_TEXT_COLOR_UNREAD;
100    private static int SENDERS_TEXT_COLOR_READ;
101    private static int SENDERS_TEXT_COLOR_UNREAD;
102    private static int DATE_TEXT_COLOR_READ;
103    private static int DATE_TEXT_COLOR_UNREAD;
104    private static int DATE_BACKGROUND_PADDING_LEFT;
105    private static int TOUCH_SLOP;
106    private static int sDateBackgroundHeight;
107    private static int sStandardScaledDimen;
108    private static CharacterStyle sLightTextStyle;
109    private static CharacterStyle sNormalTextStyle;
110
111    // Static paints.
112    private static TextPaint sPaint = new TextPaint();
113    private static TextPaint sFoldersPaint = new TextPaint();
114
115    // Backgrounds for different states.
116    private final SparseArray<Drawable> mBackgrounds = new SparseArray<Drawable>();
117
118    // Dimensions and coordinates.
119    private int mViewWidth = -1;
120    private int mMode = -1;
121    private int mDateX;
122    private int mPaperclipX;
123    private int mFoldersXEnd;
124    private int mSendersWidth;
125
126    /** Whether we're running under test mode. */
127    private boolean mTesting = false;
128    /** Whether we are on a tablet device or not */
129    private final boolean mTabletDevice;
130
131    @VisibleForTesting
132    ConversationItemViewCoordinates mCoordinates;
133
134    private final Context mContext;
135
136    private String mAccount;
137    private ConversationItemViewModel mHeader;
138    private ViewMode mViewMode;
139    private boolean mDownEvent;
140    private boolean mChecked = false;
141    private static int sFadedColor = -1;
142    private static int sFadedActivatedColor = -1;
143    private ConversationSelectionSet mSelectedConversationSet;
144    private Folder mDisplayedFolder;
145    private static Bitmap MORE_FOLDERS;
146
147    static {
148        sPaint.setAntiAlias(true);
149        sFoldersPaint.setAntiAlias(true);
150    }
151
152
153    /**
154     * Handles displaying folders in a conversation header view.
155     */
156    static class ConversationItemFolderDisplayer extends FolderDisplayer {
157        // Maximum number of folders to be displayed.
158        private static final int MAX_DISPLAYED_FOLDERS_COUNT = 4;
159
160        private int mFoldersCount;
161        private boolean mHasMoreFolders;
162
163        @Override
164        public void loadConversationFolders(Folder folder, String rawFolders) {
165            super.loadConversationFolders(folder, rawFolders);
166
167            mFoldersCount = mFolderValuesSortedSet.size();
168            mHasMoreFolders = mFoldersCount > MAX_DISPLAYED_FOLDERS_COUNT;
169            mFoldersCount = Math.min(mFoldersCount, MAX_DISPLAYED_FOLDERS_COUNT);
170        }
171
172        public boolean hasVisibleFolders() {
173            return mFoldersCount > 0;
174        }
175
176        private int measureFolders(int mode) {
177            int availableSpace = ConversationItemViewCoordinates.getFoldersWidth(mContext, mode);
178            int cellSize = ConversationItemViewCoordinates.getFolderCellWidth(mContext, mode,
179                    mFoldersCount);
180
181            int totalWidth = 0;
182            for (FolderValues labelValues : mFolderValuesSortedSet) {
183                String labelString = labelValues.name;
184                int width = (int) sFoldersPaint.measureText(labelString) + cellSize;
185                if (width % cellSize != 0) {
186                    width += cellSize - (width % cellSize);
187                }
188                totalWidth += width;
189                if (totalWidth > availableSpace) {
190                    break;
191                }
192            }
193
194            return totalWidth;
195        }
196
197        public void drawFolders(Canvas canvas, ConversationItemViewCoordinates coordinates,
198                int foldersXEnd, int mode) {
199            if (mFoldersCount == 0) {
200                return;
201            }
202
203            int xEnd = foldersXEnd;
204            int y = coordinates.foldersY - coordinates.foldersAscent;
205            int height = coordinates.foldersHeight;
206            int topPadding = coordinates.foldersTopPadding;
207            int ascent = coordinates.foldersAscent;
208            sFoldersPaint.setTextSize(coordinates.foldersFontSize);
209
210            // Initialize space and cell size based on the current mode.
211            int availableSpace = ConversationItemViewCoordinates.getFoldersWidth(mContext, mode);
212            int averageWidth = availableSpace / mFoldersCount;
213            int cellSize = ConversationItemViewCoordinates.getFolderCellWidth(mContext, mode,
214                    mFoldersCount);
215
216            // First pass to calculate the starting point.
217            int totalWidth = measureFolders(mode);
218            int xStart = xEnd - Math.min(availableSpace, totalWidth);
219
220            // Second pass to draw folders.
221            for (FolderValues labelValues : mFolderValuesSortedSet) {
222                String folderstring = labelValues.name;
223                int width = cellSize;
224                boolean labelTooLong = false;
225                width = (int) sFoldersPaint.measureText(folderstring) + cellSize;
226                if (width % cellSize != 0) {
227                    width += cellSize - (width % cellSize);
228                }
229                if (totalWidth > availableSpace && width > averageWidth) {
230                    width = averageWidth;
231                    labelTooLong = true;
232                }
233
234                // TODO (mindyp): how to we get this?
235                final boolean isMuted = false;
236                     //   labelValues.folderId == sGmail.getFolderMap(mAccount).getFolderIdIgnored();
237
238                // Draw the box.
239                sFoldersPaint.setColor(labelValues.backgroundColor);
240                sFoldersPaint.setStyle(isMuted ? Paint.Style.STROKE : Paint.Style.FILL_AND_STROKE);
241                canvas.drawRect(xStart, y + ascent, xStart + width, y + ascent + height,
242                        sFoldersPaint);
243
244                // Draw the text.
245                sFoldersPaint.setColor(labelValues.textColor);
246                int padding = getPadding(width, (int) sFoldersPaint.measureText(folderstring));
247                if (labelTooLong) {
248                    padding = cellSize / 2;
249                    int rightBorder = xStart + width - padding;
250                    Shader shader = new LinearGradient(rightBorder - padding, y, rightBorder, y,
251                            labelValues.textColor,
252                            Utils.getTransparentColor(labelValues.textColor),
253                            Shader.TileMode.CLAMP);
254                    sFoldersPaint.setShader(shader);
255                }
256                canvas.drawText(folderstring, xStart + padding, y + topPadding, sFoldersPaint);
257                sFoldersPaint.setShader(null);
258
259                availableSpace -= width;
260                xStart += width;
261                if (availableSpace <= 0 && mHasMoreFolders) {
262                    canvas.drawBitmap(MORE_FOLDERS, xEnd, y + ascent, sFoldersPaint);
263                    return;
264                }
265            }
266        }
267
268        /**
269         * Helpers function to align an element in the center of a space.
270         */
271        private static int getPadding(int space, int length) {
272            return (space - length) / 2;
273        }
274    }
275
276    public ConversationItemView(Context context, String account) {
277        super(context);
278        mContext = context.getApplicationContext();
279        mTabletDevice = Utils.useTabletUI(mContext);
280
281        mAccount = account;
282        Resources res = mContext.getResources();
283
284        if (CHECKMARK_OFF == null) {
285            // Initialize static bitmaps.
286            CHECKMARK_OFF = BitmapFactory.decodeResource(res,
287                    R.drawable.btn_check_off_normal_holo_light);
288            CHECKMARK_ON = BitmapFactory.decodeResource(res,
289                    R.drawable.btn_check_on_normal_holo_light);
290            STAR_OFF = BitmapFactory.decodeResource(res,
291                    R.drawable.btn_star_off_normal_email_holo_light);
292            STAR_ON = BitmapFactory.decodeResource(res,
293                    R.drawable.btn_star_on_normal_email_holo_light);
294            ONLY_TO_ME = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_double);
295            TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_single);
296            IMPORTANT_ONLY_TO_ME = BitmapFactory.decodeResource(res,
297                    R.drawable.ic_email_caret_double_important_unread);
298            IMPORTANT_TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res,
299                    R.drawable.ic_email_caret_single_important_unread);
300            IMPORTANT_TO_OTHERS = BitmapFactory.decodeResource(res,
301                    R.drawable.ic_email_caret_none_important_unread);
302            ATTACHMENT = BitmapFactory.decodeResource(res, R.drawable.ic_attachment_holo_light);
303            MORE_FOLDERS = BitmapFactory.decodeResource(res, R.drawable.ic_folders_more);
304            DATE_BACKGROUND = BitmapFactory.decodeResource(res, R.drawable.folder_bg_holo_light);
305
306            // Initialize colors.
307            DEFAULT_TEXT_COLOR = res.getColor(R.color.default_text_color);
308            ACTIVATED_TEXT_COLOR = res.getColor(android.R.color.white);
309            LIGHT_TEXT_COLOR = res.getColor(R.color.light_text_color);
310            DRAFT_TEXT_COLOR = res.getColor(R.color.drafts);
311            SUBJECT_TEXT_COLOR_READ = res.getColor(R.color.subject_text_color_read);
312            SUBJECT_TEXT_COLOR_UNREAD = res.getColor(R.color.subject_text_color_unread);
313            SNIPPET_TEXT_COLOR_READ = res.getColor(R.color.snippet_text_color_read);
314            SNIPPET_TEXT_COLOR_UNREAD = res.getColor(R.color.snippet_text_color_unread);
315            SENDERS_TEXT_COLOR_READ = res.getColor(R.color.senders_text_color_read);
316            SENDERS_TEXT_COLOR_UNREAD = res.getColor(R.color.senders_text_color_unread);
317            DATE_TEXT_COLOR_READ = res.getColor(R.color.date_text_color_read);
318            DATE_TEXT_COLOR_UNREAD = res.getColor(R.color.date_text_color_unread);
319            DATE_BACKGROUND_PADDING_LEFT = res
320                    .getDimensionPixelSize(R.dimen.date_background_padding_left);
321            TOUCH_SLOP = res.getDimensionPixelSize(R.dimen.touch_slop);
322            sDateBackgroundHeight = res.getDimensionPixelSize(R.dimen.date_background_height);
323            sStandardScaledDimen = res.getDimensionPixelSize(R.dimen.standard_scaled_dimen);
324
325            // Initialize static color.
326            sNormalTextStyle = new StyleSpan(Typeface.NORMAL);
327            sLightTextStyle = new ForegroundColorSpan(LIGHT_TEXT_COLOR);
328        }
329    }
330
331    public void bind(Cursor cursor, String account, ViewMode viewMode,
332            ConversationSelectionSet set, Folder folder) {
333        mAccount = account;
334        mViewMode = viewMode;
335        mHeader = ConversationItemViewModel.forCursor(account, cursor);
336        mSelectedConversationSet = set;
337        mDisplayedFolder = folder;
338        setContentDescription(mHeader.getContentDescription(mContext));
339        requestLayout();
340    }
341
342    public Conversation getConversation() {
343        return mHeader.conversation;
344    }
345
346    /**
347     * Sets the mode. Only used for testing.
348     */
349    @VisibleForTesting
350    void setMode(int mode) {
351        mMode = mode;
352        mTesting = true;
353    }
354
355    private static void startTimer(String tag) {
356        if (sTimer != null) {
357            sTimer.start(tag);
358        }
359    }
360
361    private static void pauseTimer(String tag) {
362        if (sTimer != null) {
363            sTimer.pause(tag);
364        }
365    }
366
367    @Override
368    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
369        startTimer(PERF_TAG_LAYOUT);
370
371        super.onLayout(changed, left, top, right, bottom);
372
373        int width = right - left;
374        if (width != mViewWidth) {
375            mViewWidth = width;
376            if (!mTesting) {
377                mMode = ConversationItemViewCoordinates.getMode(mContext, mViewMode);
378            }
379        }
380        mHeader.viewWidth = mViewWidth;
381        Resources res = getResources();
382        mHeader.standardScaledDimen = res.getDimensionPixelOffset(R.dimen.standard_scaled_dimen);
383        if (mHeader.standardScaledDimen != sStandardScaledDimen) {
384            // Large Text has been toggle on/off. Update the static dimens.
385            sStandardScaledDimen = mHeader.standardScaledDimen;
386            ConversationItemViewCoordinates.refreshConversationHeights(mContext);
387            sDateBackgroundHeight = res.getDimensionPixelSize(R.dimen.date_background_height);
388        }
389        mCoordinates = ConversationItemViewCoordinates.forWidth(mContext, mViewWidth, mMode,
390                mHeader.standardScaledDimen);
391        calculateTextsAndBitmaps();
392        calculateCoordinates();
393        mHeader.validate(mContext);
394
395        pauseTimer(PERF_TAG_LAYOUT);
396        if (sTimer != null && ++sLayoutCount >= PERF_LAYOUT_ITERATIONS) {
397            sTimer.dumpResults();
398            sTimer = new Timer();
399            sLayoutCount = 0;
400        }
401    }
402
403    @Override
404    public void setBackgroundResource(int resourceId) {
405        Drawable drawable = mBackgrounds.get(resourceId);
406        if (drawable == null) {
407            drawable = getResources().getDrawable(resourceId);
408            mBackgrounds.put(resourceId, drawable);
409        }
410        if (getBackground() != drawable) {
411            super.setBackgroundDrawable(drawable);
412        }
413    }
414
415    private void calculateTextsAndBitmaps() {
416        startTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
417        if (mSelectedConversationSet != null) {
418            mChecked = mSelectedConversationSet.contains(mHeader.conversation);
419        }
420        // Update font color.
421        int fontColor = getFontColor(DEFAULT_TEXT_COLOR);
422        boolean fontChanged = false;
423        if (mHeader.fontColor != fontColor) {
424            fontChanged = true;
425            mHeader.fontColor = fontColor;
426        }
427
428        boolean isUnread = mHeader.unread;
429
430        final boolean checkboxEnabled = true;
431        if (mHeader.checkboxVisible != checkboxEnabled) {
432            mHeader.checkboxVisible = checkboxEnabled;
433        }
434
435        // Update background.
436        updateBackground(isUnread);
437
438        if (mHeader.isLayoutValid(mContext)) {
439            // Relayout subject if font color has changed.
440            if (fontChanged) {
441                createSubjectSpans(isUnread);
442                layoutSubject();
443            }
444            pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
445            return;
446        }
447
448        startTimer(PERF_TAG_CALCULATE_FOLDERS);
449
450        // Initialize folder displayer.
451        if (mCoordinates.showFolders) {
452            mHeader.folderDisplayer = new ConversationItemFolderDisplayer();
453            mHeader.folderDisplayer.initialize(mContext, mAccount);
454            mHeader.folderDisplayer.loadConversationFolders(mDisplayedFolder, mHeader.rawFolders);
455        }
456
457        pauseTimer(PERF_TAG_CALCULATE_FOLDERS);
458
459        // Star.
460        mHeader.starBitmap = mHeader.starred ? STAR_ON : STAR_OFF;
461
462        // Date.
463        mHeader.dateText = DateUtils.getRelativeTimeSpanString(mContext,
464                mHeader.conversation.dateMs).toString();
465
466        // Paper clip icon.
467        mHeader.paperclip = null;
468        if (mHeader.conversation.hasAttachments) {
469            mHeader.paperclip = ATTACHMENT;
470        }
471
472        // Personal level.
473        mHeader.personalLevelBitmap = null;
474
475        startTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT);
476
477        // Subject.
478        createSubjectSpans(isUnread);
479
480        // Parse senders fragments.
481        parseSendersFragments(isUnread);
482
483        pauseTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT);
484        pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
485    }
486
487    private void createSubjectSpans(boolean isUnread) {
488        final String subject = filterTag(mHeader.conversation.subject);
489        final String snippet = mHeader.conversation.snippet;
490        int subjectColor = isUnread ? SUBJECT_TEXT_COLOR_UNREAD : SUBJECT_TEXT_COLOR_READ;
491        int snippetColor = isUnread ? SNIPPET_TEXT_COLOR_UNREAD : SNIPPET_TEXT_COLOR_READ;
492        mHeader.subjectText = new SpannableStringBuilder(mContext.getString(
493                R.string.subject_and_snippet, subject, snippet));
494        if (isUnread) {
495            mHeader.subjectText.setSpan(new StyleSpan(Typeface.BOLD), 0, subject.length(),
496                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
497        }
498        int fontColor = getFontColor(subjectColor);
499        mHeader.subjectText.setSpan(new ForegroundColorSpan(fontColor), 0,
500                subject.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
501        fontColor = getFontColor(snippetColor);
502        mHeader.subjectText.setSpan(new ForegroundColorSpan(fontColor), subject.length() + 1,
503                mHeader.subjectText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
504    }
505
506    private int getFontColor(int defaultColor) {
507        return isActivated() && mTabletDevice ? ACTIVATED_TEXT_COLOR
508                : defaultColor;
509    }
510
511    private void layoutSubject() {
512        sPaint.setTextSize(mCoordinates.subjectFontSize);
513        sPaint.setColor(mHeader.fontColor);
514        mHeader.subjectLayout = new StaticLayout(mHeader.subjectText, sPaint,
515                mCoordinates.subjectWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
516        if (mCoordinates.subjectLineCount < mHeader.subjectLayout.getLineCount()) {
517            int end = mHeader.subjectLayout.getLineEnd(mCoordinates.subjectLineCount - 1);
518            mHeader.subjectLayout = new StaticLayout(mHeader.subjectText.subSequence(0, end),
519                    sPaint, mCoordinates.subjectWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
520        }
521    }
522
523    /**
524     * Parses senders text into small fragments.
525     */
526    private void parseSendersFragments(boolean isUnread) {
527        if (TextUtils.isEmpty(mHeader.conversation.senders)) {
528            return;
529        }
530        mHeader.sendersText = formatSenders(mHeader.conversation.senders);
531        mHeader.addSenderFragment(0, mHeader.sendersText.length(), sNormalTextStyle, true);
532    }
533
534    private String formatSenders(String sendersString) {
535        String[] senders = TextUtils.split(sendersString, Address.ADDRESS_DELIMETER);
536        String[] namesOnly = new String[senders.length];
537        Rfc822Token[] senderTokens;
538        String display;
539        for (int i = 0; i < senders.length; i++) {
540            senderTokens = Rfc822Tokenizer.tokenize(senders[i]);
541            if (senderTokens != null && senderTokens.length > 0) {
542                display = senderTokens[0].getName();
543                if (TextUtils.isEmpty(display)) {
544                    display = senderTokens[0].getAddress();
545                }
546                namesOnly[i] = display;
547            }
548        }
549        return TextUtils.join(Address.ADDRESS_DELIMETER + " ", namesOnly);
550    }
551
552    private boolean canFitFragment(int width, int line, int fixedWidth) {
553        if (line == mCoordinates.sendersLineCount) {
554            return width + fixedWidth <= mSendersWidth;
555        } else {
556            return width <= mSendersWidth;
557        }
558    }
559
560    private void calculateCoordinates() {
561        startTimer(PERF_TAG_CALCULATE_COORDINATES);
562
563        sPaint.setTextSize(mCoordinates.dateFontSize);
564        sPaint.setTypeface(Typeface.DEFAULT);
565        mDateX = mCoordinates.dateXEnd - (int) sPaint.measureText(mHeader.dateText);
566
567        mPaperclipX = mDateX - ATTACHMENT.getWidth();
568
569        int cellWidth = mContext.getResources().getDimensionPixelSize(R.dimen.folder_cell_width);
570
571        if (mCoordinates.showFolders) {
572            if (ConversationItemViewCoordinates.displayFoldersAboveDate(mCoordinates.showFolders,
573                    mMode)) {
574                mFoldersXEnd = mCoordinates.dateXEnd;
575                mSendersWidth = mCoordinates.sendersWidth;
576            } else {
577                if (mHeader.paperclip != null) {
578                    mFoldersXEnd = mPaperclipX;
579                } else {
580                    mFoldersXEnd = mDateX - cellWidth / 2;
581                }
582                mSendersWidth = mFoldersXEnd - mCoordinates.sendersX - 2 * cellWidth;
583                if (mHeader.folderDisplayer.hasVisibleFolders()) {
584                    mSendersWidth -= ConversationItemViewCoordinates.getFoldersWidth(mContext,
585                            mMode);
586                }
587            }
588        } else {
589            int dateAttachmentStart = 0;
590            // Have this end near the paperclip or date, not the folders.
591            if (mHeader.paperclip != null) {
592                dateAttachmentStart = mPaperclipX;
593            } else {
594                dateAttachmentStart = mDateX;
595            }
596            mSendersWidth = dateAttachmentStart - mCoordinates.sendersX - cellWidth;
597        }
598
599        if (mHeader.isLayoutValid(mContext)) {
600            pauseTimer(PERF_TAG_CALCULATE_COORDINATES);
601            return;
602        }
603
604        // Layout subject.
605        layoutSubject();
606
607        // First pass to calculate width of each fragment.
608        int totalWidth = 0;
609        int fixedWidth = 0;
610        sPaint.setTextSize(mCoordinates.sendersFontSize);
611        sPaint.setTypeface(Typeface.DEFAULT);
612        for (SenderFragment senderFragment : mHeader.senderFragments) {
613            CharacterStyle style = senderFragment.style;
614            int start = senderFragment.start;
615            int end = senderFragment.end;
616            style.updateDrawState(sPaint);
617            senderFragment.width = (int) sPaint.measureText(mHeader.sendersText, start, end);
618            boolean isFixed = senderFragment.isFixed;
619            if (isFixed) {
620                fixedWidth += senderFragment.width;
621            }
622            totalWidth += senderFragment.width;
623        }
624
625        // Second pass to layout each fragment.
626        int sendersY = mCoordinates.sendersY - mCoordinates.sendersAscent;
627        if (!ConversationItemViewCoordinates.displaySendersInline(mMode)) {
628            sendersY += totalWidth <= mSendersWidth ? mCoordinates.sendersLineHeight / 2 : 0;
629        }
630        totalWidth = 0;
631        int currentLine = 1;
632        boolean ellipsize = false;
633        for (SenderFragment senderFragment : mHeader.senderFragments) {
634            CharacterStyle style = senderFragment.style;
635            int start = senderFragment.start;
636            int end = senderFragment.end;
637            int width = senderFragment.width;
638            boolean isFixed = senderFragment.isFixed;
639            style.updateDrawState(sPaint);
640
641            // No more width available, we'll only show fixed fragments.
642            if (ellipsize && !isFixed) {
643                senderFragment.shouldDisplay = false;
644                continue;
645            }
646
647            // New line and ellipsize text if needed.
648            senderFragment.ellipsizedText = null;
649            if (isFixed) {
650                fixedWidth -= width;
651            }
652            if (!canFitFragment(totalWidth + width, currentLine, fixedWidth)) {
653                // The text is too long, new line won't help. We have to
654                // ellipsize text.
655                if (totalWidth == 0) {
656                    ellipsize = true;
657                } else {
658                    // New line.
659                    if (currentLine < mCoordinates.sendersLineCount) {
660                        currentLine++;
661                        sendersY += mCoordinates.sendersLineHeight;
662                        totalWidth = 0;
663                        // The text is still too long, we have to ellipsize
664                        // text.
665                        if (totalWidth + width > mSendersWidth) {
666                            ellipsize = true;
667                        }
668                    } else {
669                        ellipsize = true;
670                    }
671                }
672
673                if (ellipsize) {
674                    width = mSendersWidth - totalWidth;
675                    // No more new line, we have to reserve width for fixed
676                    // fragments.
677                    if (currentLine == mCoordinates.sendersLineCount) {
678                        width -= fixedWidth;
679                    }
680                    senderFragment.ellipsizedText = TextUtils.ellipsize(
681                            mHeader.sendersText.substring(start, end), sPaint, width,
682                            TruncateAt.END).toString();
683                    width = (int) sPaint.measureText(senderFragment.ellipsizedText);
684                }
685            }
686            senderFragment.x = mCoordinates.sendersX + totalWidth;
687            senderFragment.y = sendersY;
688            senderFragment.shouldDisplay = true;
689            totalWidth += width;
690        }
691
692        pauseTimer(PERF_TAG_CALCULATE_COORDINATES);
693    }
694
695    /**
696     * If the subject contains the tag of a mailing-list (text surrounded with
697     * []), return the subject with that tag ellipsized, e.g.
698     * "[android-gmail-team] Hello" -> "[andr...] Hello"
699     */
700    private String filterTag(String subject) {
701        String result = subject;
702        String formatString = getContext().getResources().getString(R.string.filtered_tag);
703        if (!TextUtils.isEmpty(subject) && subject.charAt(0) == '[') {
704            int end = subject.indexOf(']');
705            if (end > 0) {
706                String tag = subject.substring(1, end);
707                result = String.format(formatString, Utils.ellipsize(tag, 7),
708                        subject.substring(end + 1));
709            }
710        }
711        return result;
712    }
713
714    @Override
715    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
716        int width = measureWidth(widthMeasureSpec);
717        int height = measureHeight(heightMeasureSpec,
718                ConversationItemViewCoordinates.getMode(mContext, mViewMode));
719        setMeasuredDimension(width, height);
720    }
721
722    /**
723     * Determine the width of this view.
724     *
725     * @param measureSpec A measureSpec packed into an int
726     * @return The width of the view, honoring constraints from measureSpec
727     */
728    private int measureWidth(int measureSpec) {
729        int result = 0;
730        int specMode = MeasureSpec.getMode(measureSpec);
731        int specSize = MeasureSpec.getSize(measureSpec);
732
733        if (specMode == MeasureSpec.EXACTLY) {
734            // We were told how big to be
735            result = specSize;
736        } else {
737            // Measure the text
738            result = mViewWidth;
739            if (specMode == MeasureSpec.AT_MOST) {
740                // Respect AT_MOST value if that was what is called for by
741                // measureSpec
742                result = Math.min(result, specSize);
743            }
744        }
745        return result;
746    }
747
748    /**
749     * Determine the height of this view.
750     *
751     * @param measureSpec A measureSpec packed into an int
752     * @param mode The current mode of this view
753     * @return The height of the view, honoring constraints from measureSpec
754     */
755    private int measureHeight(int measureSpec, int mode) {
756        int result = 0;
757        int specMode = MeasureSpec.getMode(measureSpec);
758        int specSize = MeasureSpec.getSize(measureSpec);
759
760        if (specMode == MeasureSpec.EXACTLY) {
761            // We were told how big to be
762            result = specSize;
763        } else {
764            // Measure the text
765            result = ConversationItemViewCoordinates.getHeight(mContext, mode);
766            if (specMode == MeasureSpec.AT_MOST) {
767                // Respect AT_MOST value if that was what is called for by
768                // measureSpec
769                result = Math.min(result, specSize);
770            }
771        }
772        return result;
773    }
774
775    @Override
776    protected void onDraw(Canvas canvas) {
777        // Check mark.
778        if (mHeader.checkboxVisible) {
779            Bitmap checkmark = mChecked ? CHECKMARK_ON : CHECKMARK_OFF;
780            canvas.drawBitmap(checkmark, mCoordinates.checkmarkX, mCoordinates.checkmarkY, sPaint);
781        }
782
783        // Personal Level.
784        if (mHeader.personalLevelBitmap != null) {
785            canvas.drawBitmap(mHeader.personalLevelBitmap, mCoordinates.personalLevelX,
786                    mCoordinates.personalLevelY, sPaint);
787        }
788
789        // Senders.
790        sPaint.setTextSize(mCoordinates.sendersFontSize);
791        sPaint.setTypeface(Typeface.DEFAULT);
792        boolean isUnread = mHeader.unread;
793        int sendersColor = getFontColor(isUnread ? SENDERS_TEXT_COLOR_UNREAD
794                : SENDERS_TEXT_COLOR_READ);
795        sPaint.setColor(sendersColor);
796        for (SenderFragment fragment : mHeader.senderFragments) {
797            if (fragment.shouldDisplay) {
798                sPaint.setTypeface(Typeface.DEFAULT);
799                fragment.style.updateDrawState(sPaint);
800                if (fragment.ellipsizedText != null) {
801                    canvas.drawText(fragment.ellipsizedText, fragment.x, fragment.y, sPaint);
802                } else {
803                    canvas.drawText(mHeader.sendersText, fragment.start, fragment.end, fragment.x,
804                            fragment.y, sPaint);
805                }
806            }
807        }
808
809        // Subject.
810        sPaint.setTextSize(mCoordinates.subjectFontSize);
811        sPaint.setTypeface(Typeface.DEFAULT);
812        sPaint.setColor(mHeader.fontColor);
813        canvas.save();
814        canvas.translate(mCoordinates.subjectX,
815                mCoordinates.subjectY + mHeader.subjectLayout.getTopPadding());
816        mHeader.subjectLayout.draw(canvas);
817        canvas.restore();
818
819        // Folders.
820        if (mCoordinates.showFolders) {
821            mHeader.folderDisplayer.drawFolders(canvas, mCoordinates, mFoldersXEnd, mMode);
822        }
823
824        // Date background: shown when there is an attachment or a visible
825        // folder.
826        if (!isActivated()
827                && mHeader.conversation.hasAttachments
828                && ConversationItemViewCoordinates.showAttachmentBackground(mMode)) {
829            mHeader.dateBackground = DATE_BACKGROUND;
830            int leftOffset = (mHeader.conversation.hasAttachments ? mPaperclipX : mDateX)
831                    - DATE_BACKGROUND_PADDING_LEFT;
832            int top = mCoordinates.showFolders ? mCoordinates.foldersY : mCoordinates.dateY;
833            Rect src = new Rect(0, 0, mHeader.dateBackground.getWidth(), mHeader.dateBackground
834                    .getHeight());
835            Rect dst = new Rect(leftOffset, top, mViewWidth, top + sDateBackgroundHeight);
836            canvas.drawBitmap(mHeader.dateBackground, src, dst, sPaint);
837        } else {
838            mHeader.dateBackground = null;
839        }
840
841        // Date.
842        sPaint.setTextSize(mCoordinates.dateFontSize);
843        sPaint.setTypeface(Typeface.DEFAULT);
844        sPaint.setColor(isUnread ? DATE_TEXT_COLOR_UNREAD : DATE_TEXT_COLOR_READ);
845        drawText(canvas, mHeader.dateText, mDateX, mCoordinates.dateY - mCoordinates.dateAscent,
846                sPaint);
847
848        // Paper clip icon.
849        if (mHeader.paperclip != null) {
850            canvas.drawBitmap(mHeader.paperclip, mPaperclipX, mCoordinates.paperclipY, sPaint);
851        }
852
853        if (mHeader.faded) {
854            int fadedColor = -1;
855            if (sFadedActivatedColor == -1) {
856                sFadedActivatedColor = mContext.getResources().getColor(
857                        R.color.faded_activated_conversation_header);
858            }
859            fadedColor = sFadedActivatedColor;
860            int restoreState = canvas.save();
861            Rect bounds = canvas.getClipBounds();
862            canvas.clipRect(bounds.left, bounds.top, bounds.right
863                    - mContext.getResources().getDimensionPixelSize(R.dimen.triangle_width),
864                    bounds.bottom);
865            canvas.drawARGB(Color.alpha(fadedColor), Color.red(fadedColor),
866                    Color.green(fadedColor), Color.blue(fadedColor));
867            canvas.restoreToCount(restoreState);
868        }
869
870        // Star.
871        canvas.drawBitmap(mHeader.starBitmap, mCoordinates.starX, mCoordinates.starY, sPaint);
872    }
873
874    private void drawText(Canvas canvas, CharSequence s, int x, int y, TextPaint paint) {
875        canvas.drawText(s, 0, s.length(), x, y, paint);
876    }
877
878    private void updateBackground(boolean isUnread) {
879        if (isUnread) {
880            if (mTabletDevice && mViewMode.getMode() == ViewMode.CONVERSATION_LIST) {
881                if (mChecked) {
882                    setBackgroundResource(R.drawable.list_conversation_wide_unread_selected_holo);
883                } else {
884                    setBackgroundResource(R.drawable.conversation_wide_unread_selector);
885                }
886            } else {
887                if (mChecked) {
888                    setCheckedActivatedBackground();
889                } else {
890                    setBackgroundResource(R.drawable.conversation_unread_selector);
891                }
892            }
893        } else {
894            if (mTabletDevice && mViewMode.getMode() == ViewMode.CONVERSATION_LIST) {
895                if (mChecked) {
896                    setBackgroundResource(R.drawable.list_conversation_wide_read_selected_holo);
897                } else {
898                    setBackgroundResource(R.drawable.conversation_wide_read_selector);
899                }
900            } else {
901                if (mChecked) {
902                    setCheckedActivatedBackground();
903                } else {
904                    setBackgroundResource(R.drawable.conversation_read_selector);
905                }
906            }
907        }
908    }
909
910    private void setCheckedActivatedBackground() {
911        if (isActivated() && mTabletDevice) {
912            setBackgroundResource(R.drawable.list_arrow_selected_holo);
913        } else {
914            setBackgroundResource(R.drawable.list_selected_holo);
915        }
916    }
917
918    /**
919     * Toggle the check mark on this view and update the conversation
920     */
921    public void toggleCheckMark() {
922        mChecked = !mChecked;
923        Conversation conv = mHeader.conversation;
924        // Set the list position of this item in the conversation
925        conv.position = mChecked ? ((ListView)getParent()).getPositionForView(this)
926                : Conversation.NO_POSITION;
927        if (mSelectedConversationSet != null) {
928            mSelectedConversationSet.toggle(conv);
929        }
930        // We update the background after the checked state has changed now that
931        // we have a selected background asset. Setting the background usually
932        // waits for a layout pass, but we don't need a full layout, just an
933        // update to the background.
934        requestLayout();
935    }
936
937    /**
938     * Toggle the star on this view and update the conversation.
939     */
940    private void toggleStar() {
941        mHeader.starred = !mHeader.starred;
942        mHeader.starBitmap = mHeader.starred ? STAR_ON : STAR_OFF;
943        postInvalidate(mCoordinates.starX, mCoordinates.starY, mCoordinates.starX
944                + mHeader.starBitmap.getWidth(),
945                mCoordinates.starY + mHeader.starBitmap.getHeight());
946        // Generalize this...
947        mHeader.conversation.updateBoolean(mContext, ConversationColumns.STARRED, mHeader.starred);
948    }
949
950    private boolean touchCheckmark(float x, float y) {
951        // Everything before senders and include a touch slop.
952        return mHeader.checkboxVisible && x < mCoordinates.sendersX + TOUCH_SLOP;
953    }
954
955    private boolean touchStar(float x, float y) {
956        // Everything after the star and include a touch slop.
957        return x > mCoordinates.starX - TOUCH_SLOP;
958    }
959
960    @Override
961    public boolean onTouchEvent(MotionEvent event) {
962        boolean handled = false;
963
964        int x = (int) event.getX();
965        int y = (int) event.getY();
966        switch (event.getAction()) {
967            case MotionEvent.ACTION_DOWN:
968                mDownEvent = true;
969                if (touchCheckmark(x, y) || touchStar(x, y)) {
970                    handled = true;
971                }
972                break;
973
974            case MotionEvent.ACTION_CANCEL:
975                mDownEvent = false;
976                break;
977
978            case MotionEvent.ACTION_UP:
979                if (mDownEvent) {
980                    if (touchCheckmark(x, y)) {
981                        // Touch on the check mark
982                        toggleCheckMark();
983                    } else if (touchStar(x, y)) {
984                        // Touch on the star
985                        toggleStar();
986                    }
987                    handled = true;
988                }
989                break;
990        }
991
992        if (!handled) {
993            handled = super.onTouchEvent(event);
994        }
995
996        return handled;
997    }
998}
999