ConversationItemView.java revision 3cb938f638cc3cec08c9c42d20192e65b1e7d343
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;
60import com.android.mail.providers.UIProvider.ConversationColumns;
61import com.android.mail.ui.ConversationSelectionSet;
62import com.android.mail.ui.FolderDisplayer;
63import com.android.mail.ui.ViewMode;
64import com.android.mail.utils.Utils;
65
66public class ConversationItemView extends View {
67    // Timer.
68    private static int sLayoutCount = 0;
69    private static Timer sTimer; // Create the sTimer here if you need to do perf analysis.
70    private static final int PERF_LAYOUT_ITERATIONS = 50;
71    private static final String PERF_TAG_LAYOUT = "CCHV.layout";
72    private static final String PERF_TAG_CALCULATE_TEXTS_BITMAPS = "CCHV.txtsbmps";
73    private static final String PERF_TAG_CALCULATE_SENDER_SUBJECT = "CCHV.sendersubj";
74    private static final String PERF_TAG_CALCULATE_FOLDERS = "CCHV.folders";
75    private static final String PERF_TAG_CALCULATE_COORDINATES = "CCHV.coordinates";
76
77    // Static bitmaps.
78    private static Bitmap CHECKMARK_OFF;
79    private static Bitmap CHECKMARK_ON;
80    private static Bitmap STAR_OFF;
81    private static Bitmap STAR_ON;
82    private static Bitmap ATTACHMENT;
83    private static Bitmap ONLY_TO_ME;
84    private static Bitmap TO_ME_AND_OTHERS;
85    private static Bitmap IMPORTANT_ONLY_TO_ME;
86    private static Bitmap IMPORTANT_TO_ME_AND_OTHERS;
87    private static Bitmap IMPORTANT_TO_OTHERS;
88    private static Bitmap DATE_BACKGROUND;
89
90    // Static colors.
91    private static int DEFAULT_TEXT_COLOR;
92    private static int ACTIVATED_TEXT_COLOR;
93    private static int LIGHT_TEXT_COLOR;
94    private static int DRAFT_TEXT_COLOR;
95    private static int SUBJECT_TEXT_COLOR_READ;
96    private static int SUBJECT_TEXT_COLOR_UNREAD;
97    private static int SNIPPET_TEXT_COLOR_READ;
98    private static int SNIPPET_TEXT_COLOR_UNREAD;
99    private static int SENDERS_TEXT_COLOR_READ;
100    private static int SENDERS_TEXT_COLOR_UNREAD;
101    private static int DATE_TEXT_COLOR_READ;
102    private static int DATE_TEXT_COLOR_UNREAD;
103    private static int DATE_BACKGROUND_PADDING_LEFT;
104    private static int TOUCH_SLOP;
105    private static int sDateBackgroundHeight;
106    private static int sStandardScaledDimen;
107    private static CharacterStyle sLightTextStyle;
108    private static CharacterStyle sNormalTextStyle;
109
110    // Static paints.
111    private static TextPaint sPaint = new TextPaint();
112    private static TextPaint sFoldersPaint = new TextPaint();
113
114    // Backgrounds for different states.
115    private final SparseArray<Drawable> mBackgrounds = new SparseArray<Drawable>();
116
117    // Dimensions and coordinates.
118    private int mViewWidth = -1;
119    private int mMode = -1;
120    private int mDateX;
121    private int mPaperclipX;
122    private int mFoldersXEnd;
123    private int mSendersWidth;
124
125    /** Whether we're running under test mode. */
126    private boolean mTesting = false;
127    /** Whether we are on a tablet device or not */
128    private final boolean mTabletDevice;
129
130    @VisibleForTesting
131    ConversationItemViewCoordinates mCoordinates;
132
133    private final Context mContext;
134
135    private String mAccount;
136    private ConversationItemViewModel mHeader;
137    private ViewMode mViewMode;
138    private boolean mDownEvent;
139    private boolean mChecked = false;
140    private static int sFadedColor = -1;
141    private static int sFadedActivatedColor = -1;
142    private ConversationSelectionSet mSelectedConversationSet;
143    private Folder mDisplayedFolder;
144    private boolean mPriorityMarkersEnabled;
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        // Personal level.
472        mHeader.personalLevelBitmap = null;
473        if (mCoordinates.showPersonalLevel) {
474            int personalLevel = mHeader.personalLevel;
475            final boolean isImportant =
476                    mHeader.priority == UIProvider.ConversationPriority.IMPORTANT;
477            // TODO(mindyp): get whether importance indicators are enabled
478            // mPriorityMarkersEnabled =
479            // persistence.getPriorityInboxArrowsEnabled(mContext, mAccount);
480            boolean useImportantMarkers = isImportant && mPriorityMarkersEnabled;
481
482            if (personalLevel == UIProvider.ConversationPersonalLevel.ONLY_TO_ME) {
483                mHeader.personalLevelBitmap = useImportantMarkers ? IMPORTANT_ONLY_TO_ME
484                        : ONLY_TO_ME;
485            } else if (personalLevel == UIProvider.ConversationPersonalLevel.TO_ME_AND_OTHERS) {
486                mHeader.personalLevelBitmap = useImportantMarkers ? IMPORTANT_TO_ME_AND_OTHERS
487                        : TO_ME_AND_OTHERS;
488            } else if (useImportantMarkers) {
489                mHeader.personalLevelBitmap = IMPORTANT_TO_OTHERS;
490            }
491        }
492
493        startTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT);
494
495        // Subject.
496        createSubjectSpans(isUnread);
497
498        // Parse senders fragments.
499        parseSendersFragments(isUnread);
500
501        pauseTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT);
502        pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
503    }
504
505    private void createSubjectSpans(boolean isUnread) {
506        final String subject = filterTag(mHeader.conversation.subject);
507        final String snippet = mHeader.conversation.snippet;
508        int subjectColor = isUnread ? SUBJECT_TEXT_COLOR_UNREAD : SUBJECT_TEXT_COLOR_READ;
509        int snippetColor = isUnread ? SNIPPET_TEXT_COLOR_UNREAD : SNIPPET_TEXT_COLOR_READ;
510        mHeader.subjectText = new SpannableStringBuilder(mContext.getString(
511                R.string.subject_and_snippet, subject, snippet));
512        if (isUnread) {
513            mHeader.subjectText.setSpan(new StyleSpan(Typeface.BOLD), 0, subject.length(),
514                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
515        }
516        int fontColor = getFontColor(subjectColor);
517        mHeader.subjectText.setSpan(new ForegroundColorSpan(fontColor), 0,
518                subject.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
519        fontColor = getFontColor(snippetColor);
520        mHeader.subjectText.setSpan(new ForegroundColorSpan(fontColor), subject.length() + 1,
521                mHeader.subjectText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
522    }
523
524    private int getFontColor(int defaultColor) {
525        return isActivated() && mTabletDevice ? ACTIVATED_TEXT_COLOR
526                : defaultColor;
527    }
528
529    private void layoutSubject() {
530        sPaint.setTextSize(mCoordinates.subjectFontSize);
531        sPaint.setColor(mHeader.fontColor);
532        mHeader.subjectLayout = new StaticLayout(mHeader.subjectText, sPaint,
533                mCoordinates.subjectWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
534        if (mCoordinates.subjectLineCount < mHeader.subjectLayout.getLineCount()) {
535            int end = mHeader.subjectLayout.getLineEnd(mCoordinates.subjectLineCount - 1);
536            mHeader.subjectLayout = new StaticLayout(mHeader.subjectText.subSequence(0, end),
537                    sPaint, mCoordinates.subjectWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
538        }
539    }
540
541    /**
542     * Parses senders text into small fragments.
543     */
544    private void parseSendersFragments(boolean isUnread) {
545        if (TextUtils.isEmpty(mHeader.conversation.senders)) {
546            return;
547        }
548        mHeader.sendersText = formatSenders(mHeader.conversation.senders);
549        mHeader.addSenderFragment(0, mHeader.sendersText.length(), sNormalTextStyle, true);
550    }
551
552    private String formatSenders(String sendersString) {
553        String[] senders = TextUtils.split(sendersString, Address.ADDRESS_DELIMETER);
554        String[] namesOnly = new String[senders.length];
555        Rfc822Token[] senderTokens;
556        String display;
557        for (int i = 0; i < senders.length; i++) {
558            senderTokens = Rfc822Tokenizer.tokenize(senders[i]);
559            if (senderTokens != null && senderTokens.length > 0) {
560                display = senderTokens[0].getName();
561                if (TextUtils.isEmpty(display)) {
562                    display = senderTokens[0].getAddress();
563                }
564                namesOnly[i] = display;
565            }
566        }
567        return TextUtils.join(Address.ADDRESS_DELIMETER + " ", namesOnly);
568    }
569
570    private boolean canFitFragment(int width, int line, int fixedWidth) {
571        if (line == mCoordinates.sendersLineCount) {
572            return width + fixedWidth <= mSendersWidth;
573        } else {
574            return width <= mSendersWidth;
575        }
576    }
577
578    private void calculateCoordinates() {
579        startTimer(PERF_TAG_CALCULATE_COORDINATES);
580
581        sPaint.setTextSize(mCoordinates.dateFontSize);
582        sPaint.setTypeface(Typeface.DEFAULT);
583        mDateX = mCoordinates.dateXEnd - (int) sPaint.measureText(mHeader.dateText);
584
585        mPaperclipX = mDateX - ATTACHMENT.getWidth();
586
587        int cellWidth = mContext.getResources().getDimensionPixelSize(R.dimen.folder_cell_width);
588
589        if (mCoordinates.showFolders) {
590            if (ConversationItemViewCoordinates.displayFoldersAboveDate(mCoordinates.showFolders,
591                    mMode)) {
592                mFoldersXEnd = mCoordinates.dateXEnd;
593                mSendersWidth = mCoordinates.sendersWidth;
594            } else {
595                if (mHeader.paperclip != null) {
596                    mFoldersXEnd = mPaperclipX;
597                } else {
598                    mFoldersXEnd = mDateX - cellWidth / 2;
599                }
600                mSendersWidth = mFoldersXEnd - mCoordinates.sendersX - 2 * cellWidth;
601                if (mHeader.folderDisplayer.hasVisibleFolders()) {
602                    mSendersWidth -= ConversationItemViewCoordinates.getFoldersWidth(mContext,
603                            mMode);
604                }
605            }
606        } else {
607            int dateAttachmentStart = 0;
608            // Have this end near the paperclip or date, not the folders.
609            if (mHeader.paperclip != null) {
610                dateAttachmentStart = mPaperclipX;
611            } else {
612                dateAttachmentStart = mDateX;
613            }
614            mSendersWidth = dateAttachmentStart - mCoordinates.sendersX - cellWidth;
615        }
616
617        if (mHeader.isLayoutValid(mContext)) {
618            pauseTimer(PERF_TAG_CALCULATE_COORDINATES);
619            return;
620        }
621
622        // Layout subject.
623        layoutSubject();
624
625        // First pass to calculate width of each fragment.
626        int totalWidth = 0;
627        int fixedWidth = 0;
628        sPaint.setTextSize(mCoordinates.sendersFontSize);
629        sPaint.setTypeface(Typeface.DEFAULT);
630        for (SenderFragment senderFragment : mHeader.senderFragments) {
631            CharacterStyle style = senderFragment.style;
632            int start = senderFragment.start;
633            int end = senderFragment.end;
634            style.updateDrawState(sPaint);
635            senderFragment.width = (int) sPaint.measureText(mHeader.sendersText, start, end);
636            boolean isFixed = senderFragment.isFixed;
637            if (isFixed) {
638                fixedWidth += senderFragment.width;
639            }
640            totalWidth += senderFragment.width;
641        }
642
643        // Second pass to layout each fragment.
644        int sendersY = mCoordinates.sendersY - mCoordinates.sendersAscent;
645        if (!ConversationItemViewCoordinates.displaySendersInline(mMode)) {
646            sendersY += totalWidth <= mSendersWidth ? mCoordinates.sendersLineHeight / 2 : 0;
647        }
648        totalWidth = 0;
649        int currentLine = 1;
650        boolean ellipsize = false;
651        for (SenderFragment senderFragment : mHeader.senderFragments) {
652            CharacterStyle style = senderFragment.style;
653            int start = senderFragment.start;
654            int end = senderFragment.end;
655            int width = senderFragment.width;
656            boolean isFixed = senderFragment.isFixed;
657            style.updateDrawState(sPaint);
658
659            // No more width available, we'll only show fixed fragments.
660            if (ellipsize && !isFixed) {
661                senderFragment.shouldDisplay = false;
662                continue;
663            }
664
665            // New line and ellipsize text if needed.
666            senderFragment.ellipsizedText = null;
667            if (isFixed) {
668                fixedWidth -= width;
669            }
670            if (!canFitFragment(totalWidth + width, currentLine, fixedWidth)) {
671                // The text is too long, new line won't help. We have to
672                // ellipsize text.
673                if (totalWidth == 0) {
674                    ellipsize = true;
675                } else {
676                    // New line.
677                    if (currentLine < mCoordinates.sendersLineCount) {
678                        currentLine++;
679                        sendersY += mCoordinates.sendersLineHeight;
680                        totalWidth = 0;
681                        // The text is still too long, we have to ellipsize
682                        // text.
683                        if (totalWidth + width > mSendersWidth) {
684                            ellipsize = true;
685                        }
686                    } else {
687                        ellipsize = true;
688                    }
689                }
690
691                if (ellipsize) {
692                    width = mSendersWidth - totalWidth;
693                    // No more new line, we have to reserve width for fixed
694                    // fragments.
695                    if (currentLine == mCoordinates.sendersLineCount) {
696                        width -= fixedWidth;
697                    }
698                    senderFragment.ellipsizedText = TextUtils.ellipsize(
699                            mHeader.sendersText.substring(start, end), sPaint, width,
700                            TruncateAt.END).toString();
701                    width = (int) sPaint.measureText(senderFragment.ellipsizedText);
702                }
703            }
704            senderFragment.x = mCoordinates.sendersX + totalWidth;
705            senderFragment.y = sendersY;
706            senderFragment.shouldDisplay = true;
707            totalWidth += width;
708        }
709
710        pauseTimer(PERF_TAG_CALCULATE_COORDINATES);
711    }
712
713    /**
714     * If the subject contains the tag of a mailing-list (text surrounded with
715     * []), return the subject with that tag ellipsized, e.g.
716     * "[android-gmail-team] Hello" -> "[andr...] Hello"
717     */
718    private String filterTag(String subject) {
719        String result = subject;
720        String formatString = getContext().getResources().getString(R.string.filtered_tag);
721        if (!TextUtils.isEmpty(subject) && subject.charAt(0) == '[') {
722            int end = subject.indexOf(']');
723            if (end > 0) {
724                String tag = subject.substring(1, end);
725                result = String.format(formatString, Utils.ellipsize(tag, 7),
726                        subject.substring(end + 1));
727            }
728        }
729        return result;
730    }
731
732    @Override
733    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
734        int width = measureWidth(widthMeasureSpec);
735        int height = measureHeight(heightMeasureSpec,
736                ConversationItemViewCoordinates.getMode(mContext, mViewMode));
737        setMeasuredDimension(width, height);
738    }
739
740    /**
741     * Determine the width of this view.
742     *
743     * @param measureSpec A measureSpec packed into an int
744     * @return The width of the view, honoring constraints from measureSpec
745     */
746    private int measureWidth(int measureSpec) {
747        int result = 0;
748        int specMode = MeasureSpec.getMode(measureSpec);
749        int specSize = MeasureSpec.getSize(measureSpec);
750
751        if (specMode == MeasureSpec.EXACTLY) {
752            // We were told how big to be
753            result = specSize;
754        } else {
755            // Measure the text
756            result = mViewWidth;
757            if (specMode == MeasureSpec.AT_MOST) {
758                // Respect AT_MOST value if that was what is called for by
759                // measureSpec
760                result = Math.min(result, specSize);
761            }
762        }
763        return result;
764    }
765
766    /**
767     * Determine the height of this view.
768     *
769     * @param measureSpec A measureSpec packed into an int
770     * @param mode The current mode of this view
771     * @return The height of the view, honoring constraints from measureSpec
772     */
773    private int measureHeight(int measureSpec, int mode) {
774        int result = 0;
775        int specMode = MeasureSpec.getMode(measureSpec);
776        int specSize = MeasureSpec.getSize(measureSpec);
777
778        if (specMode == MeasureSpec.EXACTLY) {
779            // We were told how big to be
780            result = specSize;
781        } else {
782            // Measure the text
783            result = ConversationItemViewCoordinates.getHeight(mContext, mode);
784            if (specMode == MeasureSpec.AT_MOST) {
785                // Respect AT_MOST value if that was what is called for by
786                // measureSpec
787                result = Math.min(result, specSize);
788            }
789        }
790        return result;
791    }
792
793    @Override
794    protected void onDraw(Canvas canvas) {
795        // Check mark.
796        if (mHeader.checkboxVisible) {
797            Bitmap checkmark = mChecked ? CHECKMARK_ON : CHECKMARK_OFF;
798            canvas.drawBitmap(checkmark, mCoordinates.checkmarkX, mCoordinates.checkmarkY, sPaint);
799        }
800
801        // Personal Level.
802        if (mCoordinates.showPersonalLevel && mHeader.personalLevelBitmap != null) {
803            canvas.drawBitmap(mHeader.personalLevelBitmap, mCoordinates.personalLevelX,
804                    mCoordinates.personalLevelY, sPaint);
805        }
806
807        // Senders.
808        sPaint.setTextSize(mCoordinates.sendersFontSize);
809        sPaint.setTypeface(Typeface.DEFAULT);
810        boolean isUnread = mHeader.unread;
811        int sendersColor = getFontColor(isUnread ? SENDERS_TEXT_COLOR_UNREAD
812                : SENDERS_TEXT_COLOR_READ);
813        sPaint.setColor(sendersColor);
814        for (SenderFragment fragment : mHeader.senderFragments) {
815            if (fragment.shouldDisplay) {
816                sPaint.setTypeface(Typeface.DEFAULT);
817                fragment.style.updateDrawState(sPaint);
818                if (fragment.ellipsizedText != null) {
819                    canvas.drawText(fragment.ellipsizedText, fragment.x, fragment.y, sPaint);
820                } else {
821                    canvas.drawText(mHeader.sendersText, fragment.start, fragment.end, fragment.x,
822                            fragment.y, sPaint);
823                }
824            }
825        }
826
827        // Subject.
828        sPaint.setTextSize(mCoordinates.subjectFontSize);
829        sPaint.setTypeface(Typeface.DEFAULT);
830        sPaint.setColor(mHeader.fontColor);
831        canvas.save();
832        canvas.translate(mCoordinates.subjectX,
833                mCoordinates.subjectY + mHeader.subjectLayout.getTopPadding());
834        mHeader.subjectLayout.draw(canvas);
835        canvas.restore();
836
837        // Folders.
838        if (mCoordinates.showFolders) {
839            mHeader.folderDisplayer.drawFolders(canvas, mCoordinates, mFoldersXEnd, mMode);
840        }
841
842        // Date background: shown when there is an attachment or a visible
843        // folder.
844        if (!isActivated()
845                && mHeader.conversation.hasAttachments
846                && ConversationItemViewCoordinates.showAttachmentBackground(mMode)) {
847            mHeader.dateBackground = DATE_BACKGROUND;
848            int leftOffset = (mHeader.conversation.hasAttachments ? mPaperclipX : mDateX)
849                    - DATE_BACKGROUND_PADDING_LEFT;
850            int top = mCoordinates.showFolders ? mCoordinates.foldersY : mCoordinates.dateY;
851            Rect src = new Rect(0, 0, mHeader.dateBackground.getWidth(), mHeader.dateBackground
852                    .getHeight());
853            Rect dst = new Rect(leftOffset, top, mViewWidth, top + sDateBackgroundHeight);
854            canvas.drawBitmap(mHeader.dateBackground, src, dst, sPaint);
855        } else {
856            mHeader.dateBackground = null;
857        }
858
859        // Date.
860        sPaint.setTextSize(mCoordinates.dateFontSize);
861        sPaint.setTypeface(Typeface.DEFAULT);
862        sPaint.setColor(isUnread ? DATE_TEXT_COLOR_UNREAD : DATE_TEXT_COLOR_READ);
863        drawText(canvas, mHeader.dateText, mDateX, mCoordinates.dateY - mCoordinates.dateAscent,
864                sPaint);
865
866        // Paper clip icon.
867        if (mHeader.paperclip != null) {
868            canvas.drawBitmap(mHeader.paperclip, mPaperclipX, mCoordinates.paperclipY, sPaint);
869        }
870
871        if (mHeader.faded) {
872            int fadedColor = -1;
873            if (sFadedActivatedColor == -1) {
874                sFadedActivatedColor = mContext.getResources().getColor(
875                        R.color.faded_activated_conversation_header);
876            }
877            fadedColor = sFadedActivatedColor;
878            int restoreState = canvas.save();
879            Rect bounds = canvas.getClipBounds();
880            canvas.clipRect(bounds.left, bounds.top, bounds.right
881                    - mContext.getResources().getDimensionPixelSize(R.dimen.triangle_width),
882                    bounds.bottom);
883            canvas.drawARGB(Color.alpha(fadedColor), Color.red(fadedColor),
884                    Color.green(fadedColor), Color.blue(fadedColor));
885            canvas.restoreToCount(restoreState);
886        }
887
888        // Star.
889        canvas.drawBitmap(mHeader.starBitmap, mCoordinates.starX, mCoordinates.starY, sPaint);
890    }
891
892    private void drawText(Canvas canvas, CharSequence s, int x, int y, TextPaint paint) {
893        canvas.drawText(s, 0, s.length(), x, y, paint);
894    }
895
896    private void updateBackground(boolean isUnread) {
897        if (isUnread) {
898            if (mTabletDevice && mViewMode.getMode() == ViewMode.CONVERSATION_LIST) {
899                if (mChecked) {
900                    setBackgroundResource(R.drawable.list_conversation_wide_unread_selected_holo);
901                } else {
902                    setBackgroundResource(R.drawable.conversation_wide_unread_selector);
903                }
904            } else {
905                if (mChecked) {
906                    setCheckedActivatedBackground();
907                } else {
908                    setBackgroundResource(R.drawable.conversation_unread_selector);
909                }
910            }
911        } else {
912            if (mTabletDevice && mViewMode.getMode() == ViewMode.CONVERSATION_LIST) {
913                if (mChecked) {
914                    setBackgroundResource(R.drawable.list_conversation_wide_read_selected_holo);
915                } else {
916                    setBackgroundResource(R.drawable.conversation_wide_read_selector);
917                }
918            } else {
919                if (mChecked) {
920                    setCheckedActivatedBackground();
921                } else {
922                    setBackgroundResource(R.drawable.conversation_read_selector);
923                }
924            }
925        }
926    }
927
928    private void setCheckedActivatedBackground() {
929        if (isActivated() && mTabletDevice) {
930            setBackgroundResource(R.drawable.list_arrow_selected_holo);
931        } else {
932            setBackgroundResource(R.drawable.list_selected_holo);
933        }
934    }
935
936    /**
937     * Toggle the check mark on this view and update the conversation
938     */
939    public void toggleCheckMark() {
940        mChecked = !mChecked;
941        Conversation conv = mHeader.conversation;
942        // Set the list position of this item in the conversation
943        conv.position = mChecked ? ((ListView)getParent()).getPositionForView(this)
944                : Conversation.NO_POSITION;
945        if (mSelectedConversationSet != null) {
946            mSelectedConversationSet.toggle(conv);
947        }
948        // We update the background after the checked state has changed now that
949        // we have a selected background asset. Setting the background usually
950        // waits for a layout pass, but we don't need a full layout, just an
951        // update to the background.
952        requestLayout();
953    }
954
955    /**
956     * Toggle the star on this view and update the conversation.
957     */
958    private void toggleStar() {
959        mHeader.starred = !mHeader.starred;
960        mHeader.starBitmap = mHeader.starred ? STAR_ON : STAR_OFF;
961        postInvalidate(mCoordinates.starX, mCoordinates.starY, mCoordinates.starX
962                + mHeader.starBitmap.getWidth(),
963                mCoordinates.starY + mHeader.starBitmap.getHeight());
964        // Generalize this...
965        mHeader.conversation.updateBoolean(mContext, ConversationColumns.STARRED, mHeader.starred);
966    }
967
968    private boolean touchCheckmark(float x, float y) {
969        // Everything before senders and include a touch slop.
970        return mHeader.checkboxVisible && x < mCoordinates.sendersX + TOUCH_SLOP;
971    }
972
973    private boolean touchStar(float x, float y) {
974        // Everything after the star and include a touch slop.
975        return x > mCoordinates.starX - TOUCH_SLOP;
976    }
977
978    @Override
979    public boolean onTouchEvent(MotionEvent event) {
980        boolean handled = false;
981
982        int x = (int) event.getX();
983        int y = (int) event.getY();
984        switch (event.getAction()) {
985            case MotionEvent.ACTION_DOWN:
986                mDownEvent = true;
987                if (touchCheckmark(x, y) || touchStar(x, y)) {
988                    handled = true;
989                }
990                break;
991
992            case MotionEvent.ACTION_CANCEL:
993                mDownEvent = false;
994                break;
995
996            case MotionEvent.ACTION_UP:
997                if (mDownEvent) {
998                    if (touchCheckmark(x, y)) {
999                        // Touch on the check mark
1000                        toggleCheckMark();
1001                    } else if (touchStar(x, y)) {
1002                        // Touch on the star
1003                        toggleStar();
1004                    }
1005                    handled = true;
1006                }
1007                break;
1008        }
1009
1010        if (!handled) {
1011            handled = super.onTouchEvent(event);
1012        }
1013
1014        return handled;
1015    }
1016}
1017