ConversationItemView.java revision 6e4c01675df374929d055f3dd4bd99990d9b1612
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 android.animation.Animator;
21import android.animation.Animator.AnimatorListener;
22import android.animation.AnimatorSet;
23import android.animation.ObjectAnimator;
24import android.content.ClipData;
25import android.content.ClipData.Item;
26import android.content.Context;
27import android.content.res.Resources;
28import android.database.Cursor;
29import android.graphics.Bitmap;
30import android.graphics.BitmapFactory;
31import android.graphics.Canvas;
32import android.graphics.Color;
33import android.graphics.LinearGradient;
34import android.graphics.Paint;
35import android.graphics.Point;
36import android.graphics.Rect;
37import android.graphics.Shader;
38import android.graphics.Typeface;
39import android.graphics.drawable.Drawable;
40import android.text.Layout.Alignment;
41import android.text.Spannable;
42import android.text.SpannableString;
43import android.text.SpannableStringBuilder;
44import android.text.StaticLayout;
45import android.text.TextPaint;
46import android.text.TextUtils;
47import android.text.TextUtils.TruncateAt;
48import android.text.format.DateUtils;
49import android.text.style.CharacterStyle;
50import android.text.style.ForegroundColorSpan;
51import android.text.style.StyleSpan;
52import android.util.SparseArray;
53import android.view.DragEvent;
54import android.view.MotionEvent;
55import android.view.View;
56import android.view.ViewGroup;
57import android.view.ViewGroup.LayoutParams;
58import android.view.animation.DecelerateInterpolator;
59import android.widget.TextView;
60
61import com.android.mail.R;
62import com.android.mail.browse.ConversationItemViewModel.SenderFragment;
63import com.android.mail.perf.Timer;
64import com.android.mail.providers.Conversation;
65import com.android.mail.providers.Folder;
66import com.android.mail.providers.UIProvider;
67import com.android.mail.providers.UIProvider.ConversationColumns;
68import com.android.mail.ui.AnimatedAdapter;
69import com.android.mail.ui.ControllableActivity;
70import com.android.mail.ui.ConversationSelectionSet;
71import com.android.mail.ui.FolderDisplayer;
72import com.android.mail.ui.SwipeableItemView;
73import com.android.mail.ui.SwipeableListView;
74import com.android.mail.ui.ViewMode;
75import com.android.mail.utils.LogTag;
76import com.android.mail.utils.LogUtils;
77import com.android.mail.utils.Utils;
78import com.google.common.annotations.VisibleForTesting;
79
80public class ConversationItemView extends View implements SwipeableItemView {
81    // Timer.
82    private static int sLayoutCount = 0;
83    private static Timer sTimer; // Create the sTimer here if you need to do
84                                 // perf analysis.
85    private static final int PERF_LAYOUT_ITERATIONS = 50;
86    private static final String PERF_TAG_LAYOUT = "CCHV.layout";
87    private static final String PERF_TAG_CALCULATE_TEXTS_BITMAPS = "CCHV.txtsbmps";
88    private static final String PERF_TAG_CALCULATE_SENDER_SUBJECT = "CCHV.sendersubj";
89    private static final String PERF_TAG_CALCULATE_FOLDERS = "CCHV.folders";
90    private static final String PERF_TAG_CALCULATE_COORDINATES = "CCHV.coordinates";
91    private static final String LOG_TAG = LogTag.getLogTag();
92
93    // Static bitmaps.
94    private static Bitmap CHECKMARK_OFF;
95    private static Bitmap CHECKMARK_ON;
96    private static Bitmap STAR_OFF;
97    private static Bitmap STAR_ON;
98    private static Bitmap ATTACHMENT;
99    private static Bitmap ONLY_TO_ME;
100    private static Bitmap TO_ME_AND_OTHERS;
101    private static Bitmap IMPORTANT_ONLY_TO_ME;
102    private static Bitmap IMPORTANT_TO_ME_AND_OTHERS;
103    private static Bitmap IMPORTANT_TO_OTHERS;
104    private static Bitmap DATE_BACKGROUND;
105    private static Bitmap STATE_REPLIED;
106    private static Bitmap STATE_FORWARDED;
107    private static Bitmap STATE_REPLIED_AND_FORWARDED;
108    private static Bitmap STATE_CALENDAR_INVITE;
109
110    private static String sSendersSplitToken;
111    private static String sElidedPaddingToken;
112
113    // Static colors.
114    private static int sDefaultTextColor;
115    private static int sActivatedTextColor;
116    private static int sSubjectTextColorRead;
117    private static int sSubjectTextColorUnead;
118    private static int sSnippetTextColorRead;
119    private static int sSnippetTextColorUnread;
120    private static int sSendersTextColorRead;
121    private static int sSendersTextColorUnread;
122    private static int sDateTextColor;
123    private static int sDateBackgroundPaddingLeft;
124    private static int sTouchSlop;
125    private static int sDateBackgroundHeight;
126    private static int sStandardScaledDimen;
127    private static int sShrinkAnimationDuration;
128    private static int sSlideAnimationDuration;
129    private static int sAnimatingBackgroundColor;
130
131    // Static paints.
132    private static TextPaint sPaint = new TextPaint();
133    private static TextPaint sFoldersPaint = new TextPaint();
134
135    // Backgrounds for different states.
136    private final SparseArray<Drawable> mBackgrounds = new SparseArray<Drawable>();
137
138    // Dimensions and coordinates.
139    private int mViewWidth = -1;
140    private int mMode = -1;
141    private int mDateX;
142    private int mPaperclipX;
143    private int mFoldersXEnd;
144    private int mSendersWidth;
145
146    /** Whether we're running under test mode. */
147    private boolean mTesting = false;
148    /** Whether we are on a tablet device or not */
149    private final boolean mTabletDevice;
150
151    @VisibleForTesting
152    ConversationItemViewCoordinates mCoordinates;
153
154    private final Context mContext;
155
156    public ConversationItemViewModel mHeader;
157    private boolean mDownEvent;
158    private boolean mChecked = false;
159    private static int sFadedActivatedColor = -1;
160    private ConversationSelectionSet mSelectedConversationSet;
161    private Folder mDisplayedFolder;
162    private boolean mPriorityMarkersEnabled;
163    private boolean mCheckboxesEnabled;
164    private boolean mSwipeEnabled;
165    private int mLastTouchX;
166    private int mLastTouchY;
167    private AnimatedAdapter mAdapter;
168    private int mAnimatedHeight = -1;
169    private String mAccount;
170    private ControllableActivity mActivity;
171    private CharacterStyle mActivatedTextSpan;
172    private int mBackgroundOverride = -1;
173    private static TextView sSendersTextView;
174    private static int sScrollSlop;
175    private static int sSendersTextViewTopPadding;
176    private static int sSendersTextViewHeight;
177    private static ForegroundColorSpan sActivatedTextSpan;
178    private static Bitmap sDateBackgroundAttachment;
179    private static Bitmap sDateBackgroundNoAttachment;
180    private static Bitmap MORE_FOLDERS;
181
182    static {
183        sPaint.setAntiAlias(true);
184        sFoldersPaint.setAntiAlias(true);
185    }
186
187    /**
188     * Handles displaying folders in a conversation header view.
189     */
190    static class ConversationItemFolderDisplayer extends FolderDisplayer {
191        // Maximum number of folders to be displayed.
192        private static final int MAX_DISPLAYED_FOLDERS_COUNT = 4;
193
194        private int mFoldersCount;
195        private boolean mHasMoreFolders;
196
197        public ConversationItemFolderDisplayer(Context context) {
198            super(context);
199        }
200
201        @Override
202        public void loadConversationFolders(Conversation conv, Folder ignoreFolder) {
203            super.loadConversationFolders(conv, ignoreFolder);
204
205            mFoldersCount = mFoldersSortedSet.size();
206            mHasMoreFolders = mFoldersCount > MAX_DISPLAYED_FOLDERS_COUNT;
207            mFoldersCount = Math.min(mFoldersCount, MAX_DISPLAYED_FOLDERS_COUNT);
208        }
209
210        public boolean hasVisibleFolders() {
211            return mFoldersCount > 0;
212        }
213
214        private int measureFolders(int mode) {
215            int availableSpace = ConversationItemViewCoordinates.getFoldersWidth(mContext, mode);
216            int cellSize = ConversationItemViewCoordinates.getFolderCellWidth(mContext, mode,
217                    mFoldersCount);
218
219            int totalWidth = 0;
220            for (Folder f : mFoldersSortedSet) {
221                final String folderString = f.name;
222                int width = (int) sFoldersPaint.measureText(folderString) + cellSize;
223                if (width % cellSize != 0) {
224                    width += cellSize - (width % cellSize);
225                }
226                totalWidth += width;
227                if (totalWidth > availableSpace) {
228                    break;
229                }
230            }
231
232            return totalWidth;
233        }
234
235        public void drawFolders(Canvas canvas, ConversationItemViewCoordinates coordinates,
236                int foldersXEnd, int mode) {
237            if (mFoldersCount == 0) {
238                return;
239            }
240
241            int xEnd = foldersXEnd;
242            int y = coordinates.foldersY - coordinates.foldersAscent;
243            int height = coordinates.foldersHeight;
244            int topPadding = coordinates.foldersTopPadding;
245            int ascent = coordinates.foldersAscent;
246            sFoldersPaint.setTextSize(coordinates.foldersFontSize);
247
248            // Initialize space and cell size based on the current mode.
249            int availableSpace = ConversationItemViewCoordinates.getFoldersWidth(mContext, mode);
250            int averageWidth = availableSpace / mFoldersCount;
251            int cellSize = ConversationItemViewCoordinates.getFolderCellWidth(mContext, mode,
252                    mFoldersCount);
253
254            // First pass to calculate the starting point.
255            int totalWidth = measureFolders(mode);
256            int xStart = xEnd - Math.min(availableSpace, totalWidth);
257
258            // Second pass to draw folders.
259            for (Folder f : mFoldersSortedSet) {
260                final String folderString = f.name;
261                final int fgColor = f.getForegroundColor(mDefaultFgColor);
262                final int bgColor = f.getBackgroundColor(mDefaultBgColor);
263                int width = cellSize;
264                boolean labelTooLong = false;
265                width = (int) sFoldersPaint.measureText(folderString) + cellSize;
266                if (width % cellSize != 0) {
267                    width += cellSize - (width % cellSize);
268                }
269                if (totalWidth > availableSpace && width > averageWidth) {
270                    width = averageWidth;
271                    labelTooLong = true;
272                }
273
274                // TODO (mindyp): how to we get this?
275                final boolean isMuted = false;
276                // labelValues.folderId ==
277                // sGmail.getFolderMap(mAccount).getFolderIdIgnored();
278
279                // Draw the box.
280                sFoldersPaint.setColor(bgColor);
281                sFoldersPaint.setStyle(isMuted ? Paint.Style.STROKE : Paint.Style.FILL_AND_STROKE);
282                canvas.drawRect(xStart, y + ascent, xStart + width, y + ascent + height,
283                        sFoldersPaint);
284
285                // Draw the text.
286                int padding = getPadding(width, (int) sFoldersPaint.measureText(folderString));
287                if (labelTooLong) {
288                    TextPaint shortPaint = new TextPaint();
289                    shortPaint.setColor(fgColor);
290                    shortPaint.setTextSize(coordinates.foldersFontSize);
291                    padding = cellSize / 2;
292                    int rightBorder = xStart + width - padding;
293                    Shader shader = new LinearGradient(rightBorder - padding, y, rightBorder, y,
294                            fgColor, Utils.getTransparentColor(fgColor), Shader.TileMode.CLAMP);
295                    shortPaint.setShader(shader);
296                    canvas.drawText(folderString, xStart + padding, y + topPadding, shortPaint);
297                } else {
298                    sFoldersPaint.setColor(fgColor);
299                    canvas.drawText(folderString, xStart + padding, y + topPadding, sFoldersPaint);
300                }
301
302                availableSpace -= width;
303                xStart += width;
304                if (availableSpace <= 0 && mHasMoreFolders) {
305                    canvas.drawBitmap(MORE_FOLDERS, xEnd, y + ascent, sFoldersPaint);
306                    return;
307                }
308            }
309        }
310    }
311
312    /**
313     * Helpers function to align an element in the center of a space.
314     */
315    private static int getPadding(int space, int length) {
316        return (space - length) / 2;
317    }
318
319    public ConversationItemView(Context context, String account) {
320        super(context);
321        setClickable(true);
322        setLongClickable(true);
323        mContext = context.getApplicationContext();
324        mTabletDevice = Utils.useTabletUI(mContext);
325        mAccount = account;
326        Resources res = mContext.getResources();
327
328        if (CHECKMARK_OFF == null) {
329            // Initialize static bitmaps.
330            CHECKMARK_OFF = BitmapFactory.decodeResource(res,
331                    R.drawable.btn_check_off_normal_holo_light);
332            CHECKMARK_ON = BitmapFactory.decodeResource(res,
333                    R.drawable.btn_check_on_normal_holo_light);
334            STAR_OFF = BitmapFactory.decodeResource(res,
335                    R.drawable.btn_star_off_normal_email_holo_light);
336            STAR_ON = BitmapFactory.decodeResource(res,
337                    R.drawable.btn_star_on_normal_email_holo_light);
338            ONLY_TO_ME = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_double);
339            TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_single);
340            IMPORTANT_ONLY_TO_ME = BitmapFactory.decodeResource(res,
341                    R.drawable.ic_email_caret_double_important_unread);
342            IMPORTANT_TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res,
343                    R.drawable.ic_email_caret_single_important_unread);
344            IMPORTANT_TO_OTHERS = BitmapFactory.decodeResource(res,
345                    R.drawable.ic_email_caret_none_important_unread);
346            ATTACHMENT = BitmapFactory.decodeResource(res, R.drawable.ic_attachment_holo_light);
347            MORE_FOLDERS = BitmapFactory.decodeResource(res, R.drawable.ic_folders_more);
348            DATE_BACKGROUND = BitmapFactory.decodeResource(res, R.drawable.folder_bg_holo_light);
349            STATE_REPLIED =
350                    BitmapFactory.decodeResource(res, R.drawable.ic_badge_reply_holo_light);
351            STATE_FORWARDED =
352                    BitmapFactory.decodeResource(res, R.drawable.ic_badge_forward_holo_light);
353            STATE_REPLIED_AND_FORWARDED =
354                    BitmapFactory.decodeResource(res, R.drawable.ic_badge_reply_forward_holo_light);
355            STATE_CALENDAR_INVITE =
356                    BitmapFactory.decodeResource(res, R.drawable.ic_badge_invite_holo_light);
357
358            // Initialize colors.
359            sDefaultTextColor = res.getColor(R.color.default_text_color);
360            sActivatedTextColor = res.getColor(android.R.color.white);
361            sActivatedTextSpan = new ForegroundColorSpan(sActivatedTextColor);
362            sSubjectTextColorRead = res.getColor(R.color.subject_text_color_read);
363            sSubjectTextColorUnead = res.getColor(R.color.subject_text_color_unread);
364            sSnippetTextColorRead = res.getColor(R.color.snippet_text_color_read);
365            sSnippetTextColorUnread = res.getColor(R.color.snippet_text_color_unread);
366            sSendersTextColorRead = res.getColor(R.color.senders_text_color_read);
367            sSendersTextColorUnread = res.getColor(R.color.senders_text_color_unread);
368            sDateTextColor = res.getColor(R.color.date_text_color);
369            sDateBackgroundPaddingLeft = res
370                    .getDimensionPixelSize(R.dimen.date_background_padding_left);
371            sTouchSlop = res.getDimensionPixelSize(R.dimen.touch_slop);
372            sDateBackgroundHeight = res.getDimensionPixelSize(R.dimen.date_background_height);
373            sStandardScaledDimen = res.getDimensionPixelSize(R.dimen.standard_scaled_dimen);
374            sShrinkAnimationDuration = res.getInteger(R.integer.shrink_animation_duration);
375            sSlideAnimationDuration = res.getInteger(R.integer.slide_animation_duration);
376            // Initialize static color.
377            sSendersSplitToken = res.getString(R.string.senders_split_token);
378            sElidedPaddingToken = res.getString(R.string.elided_padding_token);
379            sAnimatingBackgroundColor = res.getColor(R.color.animating_item_background_color);
380            sSendersTextViewTopPadding = res.getDimensionPixelSize
381                    (R.dimen.senders_textview_top_padding);
382            sSendersTextViewHeight = res.getDimensionPixelSize
383                    (R.dimen.senders_textview_height);
384            sScrollSlop = res.getInteger(R.integer.swipeScrollSlop);
385        }
386    }
387
388    public void bind(Cursor cursor, ControllableActivity activity, ConversationSelectionSet set,
389            Folder folder, boolean checkboxesDisabled, boolean swipeEnabled,
390            boolean priorityArrowEnabled, AnimatedAdapter adapter) {
391        bind(ConversationItemViewModel.forCursor(mAccount, cursor), activity, set, folder,
392                checkboxesDisabled, swipeEnabled, priorityArrowEnabled, adapter);
393    }
394
395    public void bind(Conversation conversation, ControllableActivity activity,
396            ConversationSelectionSet set, Folder folder, boolean checkboxesDisabled,
397            boolean swipeEnabled, boolean priorityArrowEnabled, AnimatedAdapter adapter) {
398        bind(ConversationItemViewModel.forConversation(mAccount, conversation), activity, set,
399                folder, checkboxesDisabled, swipeEnabled, priorityArrowEnabled, adapter);
400    }
401
402    private void bind(ConversationItemViewModel header, ControllableActivity activity,
403            ConversationSelectionSet set, Folder folder, boolean checkboxesDisabled,
404            boolean swipeEnabled, boolean priorityArrowEnabled, AnimatedAdapter adapter) {
405        mHeader = header;
406        mActivity = activity;
407        mSelectedConversationSet = set;
408        mDisplayedFolder = folder;
409        mCheckboxesEnabled = !checkboxesDisabled;
410        mSwipeEnabled = swipeEnabled;
411        mPriorityMarkersEnabled = priorityArrowEnabled;
412        mAdapter = adapter;
413        setContentDescription(mHeader.getContentDescription(mContext));
414        requestLayout();
415    }
416
417    /**
418     * Get the Conversation object associated with this view.
419     */
420    public Conversation getConversation() {
421        return mHeader.conversation;
422    }
423
424    /**
425     * Sets the mode. Only used for testing.
426     */
427    @VisibleForTesting
428    void setMode(int mode) {
429        mMode = mode;
430        mTesting = true;
431    }
432
433    private static void startTimer(String tag) {
434        if (sTimer != null) {
435            sTimer.start(tag);
436        }
437    }
438
439    private static void pauseTimer(String tag) {
440        if (sTimer != null) {
441            sTimer.pause(tag);
442        }
443    }
444
445    @Override
446    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
447        startTimer(PERF_TAG_LAYOUT);
448
449        super.onLayout(changed, left, top, right, bottom);
450
451        int width = right - left;
452        if (width != mViewWidth) {
453            mViewWidth = width;
454            if (!mTesting) {
455                mMode = ConversationItemViewCoordinates.getMode(mContext, mActivity.getViewMode());
456            }
457        }
458        mHeader.viewWidth = mViewWidth;
459        Resources res = getResources();
460        mHeader.standardScaledDimen = res.getDimensionPixelOffset(R.dimen.standard_scaled_dimen);
461        if (mHeader.standardScaledDimen != sStandardScaledDimen) {
462            // Large Text has been toggle on/off. Update the static dimens.
463            sStandardScaledDimen = mHeader.standardScaledDimen;
464            ConversationItemViewCoordinates.refreshConversationHeights(mContext);
465            sDateBackgroundHeight = res.getDimensionPixelSize(R.dimen.date_background_height);
466        }
467        mCoordinates = ConversationItemViewCoordinates.forWidth(mContext, mViewWidth, mMode,
468                mHeader.standardScaledDimen);
469        calculateTextsAndBitmaps();
470        calculateCoordinates();
471        if (!mHeader.isLayoutValid(mContext)) {
472            mHeader.resetContentDescription();
473            setContentDescription(mHeader.getContentDescription(mContext));
474        }
475        mHeader.validate(mContext);
476
477        pauseTimer(PERF_TAG_LAYOUT);
478        if (sTimer != null && ++sLayoutCount >= PERF_LAYOUT_ITERATIONS) {
479            sTimer.dumpResults();
480            sTimer = new Timer();
481            sLayoutCount = 0;
482        }
483    }
484
485    @Override
486    public void setBackgroundResource(int resourceId) {
487        Drawable drawable = mBackgrounds.get(resourceId);
488        if (drawable == null) {
489            drawable = getResources().getDrawable(resourceId);
490            mBackgrounds.put(resourceId, drawable);
491        }
492        if (getBackground() != drawable) {
493            super.setBackgroundDrawable(drawable);
494        }
495    }
496
497    private void calculateTextsAndBitmaps() {
498        startTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
499        if (mSelectedConversationSet != null) {
500            mChecked = mSelectedConversationSet.contains(mHeader.conversation);
501        }
502        // Update font color.
503        final int fontColor = getFontColor(sDefaultTextColor);
504        final boolean fontChanged;
505        if (mHeader.fontColor != fontColor) {
506            fontChanged = true;
507            mHeader.fontColor = fontColor;
508        } else {
509            fontChanged = false;
510        }
511
512        boolean isUnread = mHeader.unread;
513
514        final boolean checkboxEnabled = mCheckboxesEnabled;
515        if (mHeader.checkboxVisible != checkboxEnabled) {
516            mHeader.checkboxVisible = checkboxEnabled;
517        }
518
519        // Update background.
520        updateBackground(isUnread);
521
522        if (mHeader.isLayoutValid(mContext)) {
523            // Relayout subject if font color has changed.
524            if (fontChanged) {
525                layoutSubjectSpans(isUnread);
526                layoutSubject();
527                layoutSenderSpans();
528            }
529            pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
530            return;
531        }
532        startTimer(PERF_TAG_CALCULATE_FOLDERS);
533
534        // Initialize folder displayer.
535        if (mCoordinates.showFolders) {
536            mHeader.folderDisplayer = new ConversationItemFolderDisplayer(mContext);
537            mHeader.folderDisplayer.loadConversationFolders(mHeader.conversation, mDisplayedFolder);
538        }
539
540        pauseTimer(PERF_TAG_CALCULATE_FOLDERS);
541
542        mHeader.dateText = DateUtils.getRelativeTimeSpanString(mContext,
543                mHeader.conversation.dateMs).toString();
544
545        // Paper clip icon.
546        mHeader.paperclip = null;
547        if (mHeader.conversation.hasAttachments) {
548            mHeader.paperclip = ATTACHMENT;
549        }
550        // Personal level.
551        mHeader.personalLevelBitmap = null;
552        if (mCoordinates.showPersonalLevel) {
553            final int personalLevel = mHeader.conversation.personalLevel;
554            final boolean isImportant =
555                    mHeader.conversation.priority == UIProvider.ConversationPriority.IMPORTANT;
556            final boolean useImportantMarkers = isImportant && mPriorityMarkersEnabled;
557
558            if (personalLevel == UIProvider.ConversationPersonalLevel.ONLY_TO_ME) {
559                mHeader.personalLevelBitmap = useImportantMarkers ? IMPORTANT_ONLY_TO_ME
560                        : ONLY_TO_ME;
561            } else if (personalLevel == UIProvider.ConversationPersonalLevel.TO_ME_AND_OTHERS) {
562                mHeader.personalLevelBitmap = useImportantMarkers ? IMPORTANT_TO_ME_AND_OTHERS
563                        : TO_ME_AND_OTHERS;
564            } else if (useImportantMarkers) {
565                mHeader.personalLevelBitmap = IMPORTANT_TO_OTHERS;
566            }
567        }
568
569        startTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT);
570
571        // Subject.
572        layoutSubjectSpans(isUnread);
573
574        mHeader.sendersDisplayText = new SpannableStringBuilder();
575        mHeader.styledSendersString = new SpannableStringBuilder();
576
577        // Parse senders fragments.
578        if (mHeader.conversation.conversationInfo != null) {
579            Context context = getContext();
580            mHeader.messageInfoString = SendersView
581                    .createMessageInfo(context, mHeader.conversation);
582            int maxChars = ConversationItemViewCoordinates.getSubjectLength(context,
583                    ConversationItemViewCoordinates.getMode(context, mActivity.getViewMode()),
584                    mHeader.folderDisplayer != null && mHeader.folderDisplayer.mFoldersCount > 0,
585                    mHeader.conversation.hasAttachments);
586            mHeader.styledSenders = SendersView.format(context,
587                    mHeader.conversation.conversationInfo, mHeader.messageInfoString.toString(),
588                    maxChars);
589        } else {
590            SendersView.formatSenders(mHeader, getContext());
591        }
592
593        pauseTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT);
594        pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
595    }
596
597    private void layoutSenderSpans() {
598        if (isActivated() && showActivatedText()) {
599            if (mActivatedTextSpan == null) {
600                mActivatedTextSpan = getActivatedTextSpan();
601            }
602            mHeader.styledSendersString.setSpan(mActivatedTextSpan, 0,
603                    mHeader.styledMessageInfoStringOffset, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
604        } else {
605            mHeader.styledSendersString.removeSpan(mActivatedTextSpan);
606        }
607    }
608
609    private TextView getSendersTextView() {
610        if (sSendersTextView == null) {
611            TextView sendersTextView = new TextView(mContext);
612            sendersTextView.setMaxLines(1);
613            sendersTextView.setEllipsize(TextUtils.TruncateAt.END);
614            sendersTextView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
615                    ViewGroup.LayoutParams.WRAP_CONTENT));
616            sSendersTextView = sendersTextView;
617        }
618        return sSendersTextView;
619    }
620
621    private CharacterStyle getActivatedTextSpan() {
622        return CharacterStyle.wrap(sActivatedTextSpan);
623    }
624
625    private void layoutSubjectSpans(boolean isUnread) {
626        if (showActivatedText()) {
627            mHeader.subjectTextActivated = createSubject(isUnread, true);
628        }
629        mHeader.subjectText = createSubject(isUnread, false);
630    }
631
632    private SpannableStringBuilder createSubject(boolean isUnread, boolean activated) {
633        final String subject = filterTag(mHeader.conversation.subject);
634        final String snippet = mHeader.conversation.getSnippet();
635        int subjectColor = activated ? sActivatedTextColor : isUnread ? sSubjectTextColorUnead
636                : sSubjectTextColorRead;
637        int snippetColor = activated ? sActivatedTextColor : isUnread ? sSnippetTextColorUnread
638                : sSnippetTextColorRead;
639        SpannableStringBuilder subjectText = Conversation.getSubjectAndSnippetForDisplay(mContext,
640                subject, snippet);
641        if (isUnread) {
642            subjectText.setSpan(new StyleSpan(Typeface.BOLD), 0, subject.length(),
643                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
644        }
645        if (subject != null) {
646            subjectText.setSpan(new ForegroundColorSpan(subjectColor), 0, subject.length(),
647                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
648        }
649        if (!TextUtils.isEmpty(snippet)) {
650            subjectText.setSpan(new ForegroundColorSpan(snippetColor), subject.length() + 1,
651                    subjectText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
652        }
653        return subjectText;
654    }
655
656    /**
657     * Returns the resource for the text color depending on whether the element is activated or not.
658     * @param defaultColor
659     * @return
660     */
661    private int getFontColor(int defaultColor) {
662        final boolean isBackGroundBlue = isActivated() && showActivatedText();
663        return isBackGroundBlue ? sActivatedTextColor : defaultColor;
664    }
665
666    private boolean showActivatedText() {
667        // For activated elements in tablet in conversation mode, we show an activated color, since
668        // the background is dark blue for activated versus gray for non-activated.
669        final boolean isListCollapsed = mContext.getResources().getBoolean(R.bool.list_collapsed);
670        return mTabletDevice && !isListCollapsed;
671    }
672
673    private void layoutSubject() {
674        if (showActivatedText()) {
675            mHeader.subjectLayoutActivated =
676                    createSubjectLayout(true, mHeader.subjectTextActivated);
677        }
678        mHeader.subjectLayout = createSubjectLayout(false, mHeader.subjectText);
679    }
680
681    private StaticLayout createSubjectLayout(boolean activated,
682            SpannableStringBuilder subjectText) {
683        sPaint.setTextSize(mCoordinates.subjectFontSize);
684        sPaint.setColor(activated ? sActivatedTextColor
685                : mHeader.unread ? sSubjectTextColorUnead : sSubjectTextColorRead);
686        StaticLayout subjectLayout = new StaticLayout(subjectText, sPaint,
687                mCoordinates.subjectWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
688        int lineCount = subjectLayout.getLineCount();
689        if (mCoordinates.subjectLineCount < lineCount) {
690            int end = subjectLayout.getLineEnd(mCoordinates.subjectLineCount - 1);
691            subjectLayout = new StaticLayout(subjectText.subSequence(0, end), sPaint,
692                    mCoordinates.subjectWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
693        }
694        return subjectLayout;
695    }
696
697    private boolean canFitFragment(int width, int line, int fixedWidth) {
698        if (line == mCoordinates.sendersLineCount) {
699            return width + fixedWidth <= mSendersWidth;
700        } else {
701            return width <= mSendersWidth;
702        }
703    }
704
705    private void calculateCoordinates() {
706        startTimer(PERF_TAG_CALCULATE_COORDINATES);
707
708        sPaint.setTextSize(mCoordinates.dateFontSize);
709        sPaint.setTypeface(Typeface.DEFAULT);
710        mDateX = mCoordinates.dateXEnd - (int) sPaint.measureText(mHeader.dateText);
711
712        mPaperclipX = mDateX - ATTACHMENT.getWidth();
713
714        int cellWidth = mContext.getResources().getDimensionPixelSize(R.dimen.folder_cell_width);
715
716        if (ConversationItemViewCoordinates.isWideMode(mMode)) {
717            // Folders are displayed above the date.
718            mFoldersXEnd = mCoordinates.dateXEnd;
719            // In wide mode, the end of the senders should align with
720            // the start of the subject and is based on a max width.
721            mSendersWidth = mCoordinates.sendersWidth;
722        } else {
723            // In normal mode, the width is based on where the folders or date
724            // (or attachment icon) start.
725            if (mCoordinates.showFolders) {
726                if (mHeader.paperclip != null) {
727                    mFoldersXEnd = mPaperclipX;
728                } else {
729                    mFoldersXEnd = mDateX - cellWidth / 2;
730                }
731                mSendersWidth = mFoldersXEnd - mCoordinates.sendersX - 2 * cellWidth;
732                if (mHeader.folderDisplayer.hasVisibleFolders()) {
733                    mSendersWidth -= ConversationItemViewCoordinates.getFoldersWidth(mContext,
734                            mMode);
735                }
736            } else {
737                int dateAttachmentStart = 0;
738                // Have this end near the paperclip or date, not the folders.
739                if (mHeader.paperclip != null) {
740                    dateAttachmentStart = mPaperclipX;
741                } else {
742                    dateAttachmentStart = mDateX;
743                }
744                mSendersWidth = dateAttachmentStart - mCoordinates.sendersX - cellWidth;
745            }
746        }
747
748        if (mHeader.isLayoutValid(mContext)) {
749            pauseTimer(PERF_TAG_CALCULATE_COORDINATES);
750            return;
751        }
752
753        // Layout subject.
754        layoutSubject();
755
756        // Second pass to layout each fragment.
757        int sendersY = mCoordinates.sendersY - mCoordinates.sendersAscent;
758
759        if (mHeader.styledSenders != null) {
760            ellipsizeStyledSenders();
761            layoutSenderSpans();
762        } else {
763            // First pass to calculate width of each fragment.
764            int totalWidth = 0;
765            int fixedWidth = 0;
766            sPaint.setTextSize(mCoordinates.sendersFontSize);
767            sPaint.setTypeface(Typeface.DEFAULT);
768            for (SenderFragment senderFragment : mHeader.senderFragments) {
769                CharacterStyle style = senderFragment.style;
770                int start = senderFragment.start;
771                int end = senderFragment.end;
772                style.updateDrawState(sPaint);
773                senderFragment.width = (int) sPaint.measureText(mHeader.sendersText, start, end);
774                boolean isFixed = senderFragment.isFixed;
775                if (isFixed) {
776                    fixedWidth += senderFragment.width;
777                }
778                totalWidth += senderFragment.width;
779            }
780
781            if (!ConversationItemViewCoordinates.displaySendersInline(mMode)) {
782                sendersY += totalWidth <= mSendersWidth ? mCoordinates.sendersLineHeight / 2 : 0;
783            }
784            if (mSendersWidth < 0) {
785                mSendersWidth = 0;
786            }
787            totalWidth = ellipsize(fixedWidth, sendersY);
788            mHeader.sendersDisplayLayout = new StaticLayout(mHeader.sendersDisplayText, sPaint,
789                    mSendersWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
790        }
791
792        sPaint.setTextSize(mCoordinates.sendersFontSize);
793        sPaint.setTypeface(Typeface.DEFAULT);
794        if (mSendersWidth < 0) {
795            mSendersWidth = 0;
796        }
797
798        pauseTimer(PERF_TAG_CALCULATE_COORDINATES);
799    }
800
801    // The rules for displaying ellipsized senders are as follows:
802    // 1) If there is message info (either a COUNT or DRAFT info to display), it MUST be shown
803    // 2) If senders do not fit, ellipsize the last one that does fit, and stop
804    // appending new senders
805    private int ellipsizeStyledSenders() {
806        SpannableStringBuilder builder = new SpannableStringBuilder();
807        float totalWidth = 0;
808        boolean ellipsize = false;
809        float width;
810        SpannableStringBuilder messageInfoString =  mHeader.messageInfoString;
811        if (messageInfoString.length() > 0) {
812            CharacterStyle[] spans = messageInfoString.getSpans(0, messageInfoString.length(),
813                    CharacterStyle.class);
814            // There is only 1 character style span; make sure we apply all the
815            // styles to the paint object before measuring.
816            if (spans.length > 0) {
817                spans[0].updateDrawState(sPaint);
818            }
819            // Paint the message info string to see if we lose space.
820            float messageInfoWidth = sPaint.measureText(messageInfoString.toString());
821            totalWidth += messageInfoWidth;
822        }
823       SpannableString prevSender = null;
824       SpannableString ellipsizedText;
825        for (SpannableString sender : mHeader.styledSenders) {
826            // There may be null sender strings if there were dupes we had to remove.
827            if (sender == null) {
828                continue;
829            }
830            // No more width available, we'll only show fixed fragments.
831            if (ellipsize) {
832                break;
833            }
834            CharacterStyle[] spans = sender.getSpans(0, sender.length(), CharacterStyle.class);
835            // There is only 1 character style span.
836            if (spans.length > 0) {
837                spans[0].updateDrawState(sPaint);
838            }
839            // If there are already senders present in this string, we need to
840            // make sure we prepend the dividing token
841            if (SendersView.sElidedString.equals(sender.toString())) {
842                prevSender = sender;
843                sender = copyStyles(spans, sElidedPaddingToken + sender + sElidedPaddingToken);
844            } else if (builder.length() > 0
845                    && (prevSender == null || !SendersView.sElidedString.equals(prevSender
846                            .toString()))) {
847                prevSender = sender;
848                sender = copyStyles(spans, sSendersSplitToken + sender);
849            } else {
850                prevSender = sender;
851            }
852            if (spans.length > 0) {
853                spans[0].updateDrawState(sPaint);
854            }
855            // Measure the width of the current sender and make sure we have space
856            width = (int) sPaint.measureText(sender.toString());
857            if (width + totalWidth > mSendersWidth) {
858                // The text is too long, new line won't help. We have to
859                // ellipsize text.
860                ellipsize = true;
861                width = mSendersWidth - totalWidth; // ellipsis width?
862                ellipsizedText = copyStyles(spans,
863                        TextUtils.ellipsize(sender, sPaint, width, TruncateAt.END));
864                width = (int) sPaint.measureText(ellipsizedText.toString());
865            } else {
866                ellipsizedText = null;
867            }
868            totalWidth += width;
869
870            final CharSequence fragmentDisplayText;
871            if (ellipsizedText != null) {
872                fragmentDisplayText = ellipsizedText;
873            } else {
874                fragmentDisplayText = sender;
875            }
876            builder.append(fragmentDisplayText);
877        }
878        mHeader.styledMessageInfoStringOffset = builder.length();
879        if (messageInfoString != null) {
880            builder.append(messageInfoString);
881        }
882        mHeader.styledSendersString = builder;
883        return (int)totalWidth;
884    }
885
886    private SpannableString copyStyles(CharacterStyle[] spans, CharSequence newText) {
887        SpannableString s = new SpannableString(newText);
888        if (spans != null && spans.length > 0) {
889            s.setSpan(spans[0], 0, s.length(), 0);
890        }
891        return s;
892    }
893
894    private int ellipsize(int fixedWidth, int sendersY) {
895        int totalWidth = 0;
896        int currentLine = 1;
897        boolean ellipsize = false;
898        for (SenderFragment senderFragment : mHeader.senderFragments) {
899            CharacterStyle style = senderFragment.style;
900            int start = senderFragment.start;
901            int end = senderFragment.end;
902            int width = senderFragment.width;
903            boolean isFixed = senderFragment.isFixed;
904            style.updateDrawState(sPaint);
905
906            // No more width available, we'll only show fixed fragments.
907            if (ellipsize && !isFixed) {
908                senderFragment.shouldDisplay = false;
909                continue;
910            }
911
912            // New line and ellipsize text if needed.
913            senderFragment.ellipsizedText = null;
914            if (isFixed) {
915                fixedWidth -= width;
916            }
917            if (!canFitFragment(totalWidth + width, currentLine, fixedWidth)) {
918                // The text is too long, new line won't help. We have to
919                // ellipsize text.
920                if (totalWidth == 0) {
921                    ellipsize = true;
922                } else {
923                    // New line.
924                    if (currentLine < mCoordinates.sendersLineCount) {
925                        currentLine++;
926                        sendersY += mCoordinates.sendersLineHeight;
927                        totalWidth = 0;
928                        // The text is still too long, we have to ellipsize
929                        // text.
930                        if (totalWidth + width > mSendersWidth) {
931                            ellipsize = true;
932                        }
933                    } else {
934                        ellipsize = true;
935                    }
936                }
937
938                if (ellipsize) {
939                    width = mSendersWidth - totalWidth;
940                    // No more new line, we have to reserve width for fixed
941                    // fragments.
942                    if (currentLine == mCoordinates.sendersLineCount) {
943                        width -= fixedWidth;
944                    }
945                    senderFragment.ellipsizedText = TextUtils.ellipsize(
946                            mHeader.sendersText.substring(start, end), sPaint, width,
947                            TruncateAt.END).toString();
948                    width = (int) sPaint.measureText(senderFragment.ellipsizedText);
949                }
950            }
951            senderFragment.shouldDisplay = true;
952            totalWidth += width;
953
954            final CharSequence fragmentDisplayText;
955            if (senderFragment.ellipsizedText != null) {
956                fragmentDisplayText = senderFragment.ellipsizedText;
957            } else {
958                fragmentDisplayText = mHeader.sendersText.substring(start, end);
959            }
960            final int spanStart = mHeader.sendersDisplayText.length();
961            mHeader.sendersDisplayText.append(fragmentDisplayText);
962            mHeader.sendersDisplayText.setSpan(senderFragment.style, spanStart,
963                    mHeader.sendersDisplayText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
964        }
965        return totalWidth;
966    }
967
968    /**
969     * If the subject contains the tag of a mailing-list (text surrounded with
970     * []), return the subject with that tag ellipsized, e.g.
971     * "[android-gmail-team] Hello" -> "[andr...] Hello"
972     */
973    private String filterTag(String subject) {
974        String result = subject;
975        String formatString = getContext().getResources().getString(R.string.filtered_tag);
976        if (!TextUtils.isEmpty(subject) && subject.charAt(0) == '[') {
977            int end = subject.indexOf(']');
978            if (end > 0) {
979                String tag = subject.substring(1, end);
980                result = String.format(formatString, Utils.ellipsize(tag, 7),
981                        subject.substring(end + 1));
982            }
983        }
984        return result;
985    }
986
987    @Override
988    protected void onDraw(Canvas canvas) {
989        // Check mark.
990        if (mHeader.checkboxVisible) {
991            Bitmap checkmark = mChecked ? CHECKMARK_ON : CHECKMARK_OFF;
992            canvas.drawBitmap(checkmark, mCoordinates.checkmarkX, mCoordinates.checkmarkY, sPaint);
993        }
994
995        // Personal Level.
996        if (mCoordinates.showPersonalLevel && mHeader.personalLevelBitmap != null) {
997            canvas.drawBitmap(mHeader.personalLevelBitmap, mCoordinates.personalLevelX,
998                    mCoordinates.personalLevelY, sPaint);
999        }
1000
1001        // Senders.
1002        boolean isUnread = mHeader.unread;
1003        // Old style senders; apply text colors/ sizes/ styling.
1004        canvas.save();
1005        if (mHeader.sendersDisplayLayout != null) {
1006            sPaint.setTextSize(mCoordinates.sendersFontSize);
1007            sPaint.setTypeface(SendersView.getTypeface(isUnread));
1008            sPaint.setColor(getFontColor(isUnread ? sSendersTextColorUnread
1009                    : sSendersTextColorRead));
1010            canvas.translate(mCoordinates.sendersX,
1011                    mCoordinates.sendersY + mHeader.sendersDisplayLayout.getTopPadding());
1012            mHeader.sendersDisplayLayout.draw(canvas);
1013        } else {
1014            canvas.translate(mCoordinates.sendersX,
1015                    mCoordinates.sendersY + sSendersTextViewTopPadding);
1016            mHeader.sendersTextView = getSendersTextView();
1017            int length = (int) sPaint.measureText(mHeader.styledSendersString.toString());
1018            mHeader.sendersTextView.setText(mHeader.styledSendersString,
1019                    TextView.BufferType.SPANNABLE);
1020            mHeader.sendersTextView.setWidth(length);
1021            mHeader.sendersTextView.layout(0, 0, mSendersWidth, sSendersTextViewHeight);
1022            mHeader.sendersTextView.draw(canvas);
1023        }
1024        canvas.restore();
1025
1026
1027        // Subject.
1028        sPaint.setTextSize(mCoordinates.subjectFontSize);
1029        sPaint.setTypeface(Typeface.DEFAULT);
1030        canvas.save();
1031        if (isActivated() && showActivatedText()) {
1032            if (mHeader.subjectLayoutActivated != null) {
1033                canvas.translate(mCoordinates.subjectX, mCoordinates.subjectY
1034                        + mHeader.subjectLayoutActivated.getTopPadding());
1035                mHeader.subjectLayoutActivated.draw(canvas);
1036            }
1037        } else if (mHeader.subjectLayout != null) {
1038            canvas.translate(mCoordinates.subjectX,
1039                    mCoordinates.subjectY + mHeader.subjectLayout.getTopPadding());
1040            mHeader.subjectLayout.draw(canvas);
1041        }
1042        canvas.restore();
1043
1044        // Folders.
1045        if (mCoordinates.showFolders) {
1046            mHeader.folderDisplayer.drawFolders(canvas, mCoordinates, mFoldersXEnd, mMode);
1047        }
1048
1049        // If this folder has a color (combined view/Email), show it here
1050        if (mHeader.conversation.color != 0) {
1051            sFoldersPaint.setColor(mHeader.conversation.color);
1052            sFoldersPaint.setStyle(Paint.Style.FILL);
1053            int width = ConversationItemViewCoordinates.getColorBlockWidth(mContext);
1054            int height = ConversationItemViewCoordinates.getColorBlockHeight(mContext);
1055            canvas.drawRect(mCoordinates.dateXEnd - width, 0, mCoordinates.dateXEnd,
1056                    height, sFoldersPaint);
1057        }
1058
1059        // Date background: shown when there is an attachment or a visible
1060        // folder.
1061        if (!isActivated()
1062                && (mHeader.conversation.hasAttachments ||
1063                        (mHeader.folderDisplayer != null
1064                            && mHeader.folderDisplayer.hasVisibleFolders()))
1065                && ConversationItemViewCoordinates.showAttachmentBackground(mMode)) {
1066            int leftOffset = (mHeader.conversation.hasAttachments ? mPaperclipX : mDateX)
1067                    - sDateBackgroundPaddingLeft;
1068            int top = mCoordinates.showFolders ? mCoordinates.foldersY : mCoordinates.dateY;
1069            mHeader.dateBackground = getDateBackground(mHeader.conversation.hasAttachments);
1070            canvas.drawBitmap(mHeader.dateBackground, leftOffset, top, sPaint);
1071        } else {
1072            mHeader.dateBackground = null;
1073        }
1074
1075        // Draw the reply state. Draw nothing if neither replied nor forwarded.
1076        if (mCoordinates.showReplyState) {
1077            if (mHeader.hasBeenRepliedTo && mHeader.hasBeenForwarded) {
1078                canvas.drawBitmap(STATE_REPLIED_AND_FORWARDED, mCoordinates.replyStateX,
1079                        mCoordinates.replyStateY, null);
1080            } else if (mHeader.hasBeenRepliedTo) {
1081                canvas.drawBitmap(STATE_REPLIED, mCoordinates.replyStateX,
1082                        mCoordinates.replyStateY, null);
1083            } else if (mHeader.hasBeenForwarded) {
1084                canvas.drawBitmap(STATE_FORWARDED, mCoordinates.replyStateX,
1085                        mCoordinates.replyStateY, null);
1086            } else if (mHeader.isInvite) {
1087                canvas.drawBitmap(STATE_CALENDAR_INVITE, mCoordinates.replyStateX,
1088                        mCoordinates.replyStateY, null);
1089            }
1090        }
1091
1092        // Date.
1093        sPaint.setTextSize(mCoordinates.dateFontSize);
1094        sPaint.setTypeface(Typeface.DEFAULT);
1095        sPaint.setColor(sDateTextColor);
1096        drawText(canvas, mHeader.dateText, mDateX, mCoordinates.dateY - mCoordinates.dateAscent,
1097                sPaint);
1098
1099        // Paper clip icon.
1100        if (mHeader.paperclip != null) {
1101            canvas.drawBitmap(mHeader.paperclip, mPaperclipX, mCoordinates.paperclipY, sPaint);
1102        }
1103
1104        if (mHeader.faded) {
1105            int fadedColor = -1;
1106            if (sFadedActivatedColor == -1) {
1107                sFadedActivatedColor = mContext.getResources().getColor(
1108                        R.color.faded_activated_conversation_header);
1109            }
1110            fadedColor = sFadedActivatedColor;
1111            int restoreState = canvas.save();
1112            Rect bounds = canvas.getClipBounds();
1113            canvas.clipRect(bounds.left, bounds.top, bounds.right
1114                    - mContext.getResources().getDimensionPixelSize(R.dimen.triangle_width),
1115                    bounds.bottom);
1116            canvas.drawARGB(Color.alpha(fadedColor), Color.red(fadedColor),
1117                    Color.green(fadedColor), Color.blue(fadedColor));
1118            canvas.restoreToCount(restoreState);
1119        }
1120
1121        // Star.
1122        canvas.drawBitmap(getStarBitmap(), mCoordinates.starX, mCoordinates.starY, sPaint);
1123    }
1124
1125    private Bitmap getStarBitmap() {
1126        return mHeader.conversation.starred ? STAR_ON : STAR_OFF;
1127    }
1128
1129    private Bitmap getDateBackground(boolean hasAttachments) {
1130        int leftOffset = (hasAttachments ? mPaperclipX : mDateX) - sDateBackgroundPaddingLeft;
1131        if (hasAttachments) {
1132            if (sDateBackgroundAttachment == null) {
1133                sDateBackgroundAttachment = Bitmap.createScaledBitmap(DATE_BACKGROUND, mViewWidth
1134                        - leftOffset, sDateBackgroundHeight, false);
1135            }
1136            return sDateBackgroundAttachment;
1137        } else {
1138            if (sDateBackgroundNoAttachment == null) {
1139                sDateBackgroundNoAttachment = Bitmap.createScaledBitmap(DATE_BACKGROUND, mViewWidth
1140                        - leftOffset, sDateBackgroundHeight, false);
1141            }
1142            return sDateBackgroundNoAttachment;
1143        }
1144    }
1145
1146    private void drawText(Canvas canvas, CharSequence s, int x, int y, TextPaint paint) {
1147        canvas.drawText(s, 0, s.length(), x, y, paint);
1148    }
1149
1150    private void updateBackground(boolean isUnread) {
1151        if (mBackgroundOverride != -1) {
1152            // If the item is animating, we use a color to avoid shrinking a 9-patch
1153            // and getting weird artifacts from the overlap.
1154            setBackgroundColor(mBackgroundOverride);
1155            return;
1156        }
1157        final boolean isListOnTablet = mTabletDevice && mActivity.getViewMode().isListMode();
1158        if (isUnread) {
1159            if (isListOnTablet) {
1160                if (mChecked) {
1161                    setBackgroundResource(R.drawable.list_conversation_wide_unread_selected_holo);
1162                } else {
1163                    setBackgroundResource(R.drawable.conversation_wide_unread_selector);
1164                }
1165            } else {
1166                if (mChecked) {
1167                    setCheckedActivatedBackground();
1168                } else {
1169                    setBackgroundResource(R.drawable.conversation_unread_selector);
1170                }
1171            }
1172        } else {
1173            if (isListOnTablet) {
1174                if (mChecked) {
1175                    setBackgroundResource(R.drawable.list_conversation_wide_read_selected_holo);
1176                } else {
1177                    setBackgroundResource(R.drawable.conversation_wide_read_selector);
1178                }
1179            } else {
1180                if (mChecked) {
1181                    setCheckedActivatedBackground();
1182                } else {
1183                    setBackgroundResource(R.drawable.conversation_read_selector);
1184                }
1185            }
1186        }
1187    }
1188
1189    private void setCheckedActivatedBackground() {
1190        if (isActivated() && mTabletDevice) {
1191            setBackgroundResource(R.drawable.list_arrow_selected_holo);
1192        } else {
1193            setBackgroundResource(R.drawable.list_selected_holo);
1194        }
1195    }
1196
1197    /**
1198     * Toggle the check mark on this view and update the conversation or begin
1199     * drag, if drag is enabled.
1200     */
1201    public void toggleCheckMarkOrBeginDrag() {
1202        ViewMode mode = mActivity.getViewMode();
1203        if (!mTabletDevice || !mode.isListMode()) {
1204            toggleCheckMark();
1205        } else {
1206            beginDragMode();
1207        }
1208    }
1209
1210    private void toggleCheckMark() {
1211        if (mHeader != null && mHeader.conversation != null) {
1212            mChecked = !mChecked;
1213            Conversation conv = mHeader.conversation;
1214            // Set the list position of this item in the conversation
1215            SwipeableListView listView = getListView();
1216            conv.position = mChecked && listView != null ? listView.getPositionForView(this)
1217                    : Conversation.NO_POSITION;
1218            if (mSelectedConversationSet != null) {
1219                mSelectedConversationSet.toggle(this, conv);
1220            }
1221            if (mSelectedConversationSet.isEmpty()) {
1222                listView.commitDestructiveActions(true);
1223            }
1224            // We update the background after the checked state has changed
1225            // now that we have a selected background asset. Setting the background
1226            // usually waits for a layout pass, but we don't need a full layout,
1227            // just an update to the background.
1228            requestLayout();
1229        }
1230    }
1231
1232    /**
1233     * Return if the checkbox for this item is checked.
1234     */
1235    public boolean isChecked() {
1236        return mChecked;
1237    }
1238
1239    /**
1240     * Toggle the star on this view and update the conversation.
1241     */
1242    public void toggleStar() {
1243        mHeader.conversation.starred = !mHeader.conversation.starred;
1244        Bitmap starBitmap = getStarBitmap();
1245        postInvalidate(mCoordinates.starX, mCoordinates.starY, mCoordinates.starX
1246                + starBitmap.getWidth(),
1247                mCoordinates.starY + starBitmap.getHeight());
1248        ConversationCursor cursor = (ConversationCursor)mAdapter.getCursor();
1249        cursor.updateBoolean(mContext, mHeader.conversation, ConversationColumns.STARRED,
1250                mHeader.conversation.starred);
1251    }
1252
1253    private boolean isTouchInCheckmark(float x, float y) {
1254        // Everything before senders and include a touch slop.
1255        return mHeader.checkboxVisible && x < mCoordinates.sendersX + sTouchSlop;
1256    }
1257
1258    private boolean isTouchInStar(float x, float y) {
1259        // Everything after the star and include a touch slop.
1260        return x > mCoordinates.starX - sTouchSlop;
1261    }
1262
1263    @Override
1264    public boolean canChildBeDismissed() {
1265        return true;
1266    }
1267
1268    @Override
1269    public void dismiss() {
1270        SwipeableListView listView = getListView();
1271        if (listView != null) {
1272            getListView().dismissChild(this);
1273        }
1274    }
1275
1276    private boolean onTouchEventNoSwipe(MotionEvent event) {
1277        boolean handled = false;
1278
1279        int x = (int) event.getX();
1280        int y = (int) event.getY();
1281        mLastTouchX = x;
1282        mLastTouchY = y;
1283        switch (event.getAction()) {
1284            case MotionEvent.ACTION_DOWN:
1285                if (isTouchInCheckmark(x, y) || isTouchInStar(x, y)) {
1286                    mDownEvent = true;
1287                    handled = true;
1288                }
1289                break;
1290
1291            case MotionEvent.ACTION_CANCEL:
1292                mDownEvent = false;
1293                break;
1294
1295            case MotionEvent.ACTION_UP:
1296                if (mDownEvent) {
1297                    if (isTouchInCheckmark(x, y)) {
1298                        // Touch on the check mark
1299                        toggleCheckMark();
1300                    } else if (isTouchInStar(x, y)) {
1301                        // Touch on the star
1302                        toggleStar();
1303                    }
1304                    handled = true;
1305                }
1306                break;
1307        }
1308
1309        if (!handled) {
1310            handled = super.onTouchEvent(event);
1311        }
1312
1313        return handled;
1314    }
1315
1316    /**
1317     * ConversationItemView is given the first chance to handle touch events.
1318     */
1319    @Override
1320    public boolean onTouchEvent(MotionEvent event) {
1321        int x = (int) event.getX();
1322        int y = (int) event.getY();
1323        mLastTouchX = x;
1324        mLastTouchY = y;
1325        if (!mSwipeEnabled) {
1326            return onTouchEventNoSwipe(event);
1327        }
1328        switch (event.getAction()) {
1329            case MotionEvent.ACTION_DOWN:
1330                if (isTouchInCheckmark(x, y) || isTouchInStar(x, y)) {
1331                    mDownEvent = true;
1332                    return true;
1333                }
1334                break;
1335            case MotionEvent.ACTION_UP:
1336                if (mDownEvent) {
1337                    if (isTouchInCheckmark(x, y)) {
1338                        // Touch on the check mark
1339                        mDownEvent = false;
1340                        toggleCheckMark();
1341                        return true;
1342                    } else if (isTouchInStar(x, y)) {
1343                        // Touch on the star
1344                        mDownEvent = false;
1345                        toggleStar();
1346                        return true;
1347                    }
1348                }
1349                break;
1350        }
1351        // Let View try to handle it as well.
1352        boolean handled = super.onTouchEvent(event);
1353        if (event.getAction() == MotionEvent.ACTION_DOWN) {
1354            return true;
1355        }
1356        return handled;
1357    }
1358
1359    @Override
1360    public boolean performClick() {
1361        boolean handled = super.performClick();
1362        SwipeableListView list = getListView();
1363        if (list != null && list.getAdapter() != null) {
1364            int pos = list.findConversation(this, mHeader.conversation);
1365            list.performItemClick(this, pos, mHeader.conversation.id);
1366        }
1367        return handled;
1368    }
1369
1370    private SwipeableListView getListView() {
1371        SwipeableListView v = (SwipeableListView) ((SwipeableConversationItemView) getParent())
1372                .getListView();
1373        if (v == null) {
1374            v = mAdapter.getListView();
1375        }
1376        return v;
1377    }
1378
1379    /**
1380     * Reset any state associated with this conversation item view so that it
1381     * can be reused.
1382     */
1383    public void reset() {
1384        mBackgroundOverride = -1;
1385        setAlpha(1);
1386        setTranslationX(0);
1387        setAnimatedHeight(-1);
1388        setMinimumHeight(ConversationItemViewCoordinates.getMinHeight(mContext,
1389                mActivity.getViewMode()));
1390    }
1391
1392    /**
1393     * Grow the height of the item and fade it in when bringing a conversation
1394     * back from a destructive action.
1395     * @param listener
1396     */
1397    public void startSwipeUndoAnimation(ViewMode viewMode, final AnimatorListener listener) {
1398        ObjectAnimator undoAnimator = createTranslateXAnimation(true);
1399        undoAnimator.addListener(listener);
1400        undoAnimator.start();
1401    }
1402
1403    /**
1404     * Grow the height of the item and fade it in when bringing a conversation
1405     * back from a destructive action.
1406     * @param listener
1407     */
1408    public void startUndoAnimation(ViewMode viewMode, final AnimatorListener listener) {
1409        int minHeight = ConversationItemViewCoordinates.getMinHeight(mContext, viewMode);
1410        setMinimumHeight(minHeight);
1411        mAnimatedHeight = 0;
1412        ObjectAnimator height = createHeightAnimation(true);
1413        Animator fade = ObjectAnimator.ofFloat(this, "itemAlpha", 0, 1.0f);
1414        fade.setDuration(sShrinkAnimationDuration);
1415        fade.setInterpolator(new DecelerateInterpolator(2.0f));
1416        AnimatorSet transitionSet = new AnimatorSet();
1417        transitionSet.playTogether(height, fade);
1418        transitionSet.addListener(listener);
1419        transitionSet.start();
1420    }
1421
1422    /**
1423     * Grow the height of the item and fade it in when bringing a conversation
1424     * back from a destructive action.
1425     * @param listener
1426     */
1427    public void startDestroyWithSwipeAnimation(final AnimatorListener listener) {
1428        ObjectAnimator slide = createTranslateXAnimation(false);
1429        ObjectAnimator height = createHeightAnimation(false);
1430        AnimatorSet transitionSet = new AnimatorSet();
1431        transitionSet.playSequentially(slide, height);
1432        transitionSet.addListener(listener);
1433        transitionSet.start();
1434    }
1435
1436    private ObjectAnimator createTranslateXAnimation(boolean show) {
1437        SwipeableListView parent = getListView();
1438        // If we can't get the parent...we have bigger problems.
1439        int width = parent != null ? parent.getMeasuredWidth() : 0;
1440        final float start = show ? width : 0f;
1441        final float end = show ? 0f : width;
1442        ObjectAnimator slide = ObjectAnimator.ofFloat(this, "translationX", start, end);
1443        slide.setInterpolator(new DecelerateInterpolator(2.0f));
1444        slide.setDuration(sSlideAnimationDuration);
1445        return slide;
1446    }
1447
1448    public void startDestroyAnimation(final AnimatorListener listener) {
1449        ObjectAnimator height = createHeightAnimation(false);
1450        int minHeight = ConversationItemViewCoordinates.getMinHeight(mContext,
1451                mActivity.getViewMode());
1452        setMinimumHeight(0);
1453        mBackgroundOverride = sAnimatingBackgroundColor;
1454        setBackgroundColor(mBackgroundOverride);
1455        mAnimatedHeight = minHeight;
1456        height.addListener(listener);
1457        height.start();
1458    }
1459
1460    private ObjectAnimator createHeightAnimation(boolean show) {
1461        int minHeight = ConversationItemViewCoordinates.getMinHeight(getContext(),
1462                mActivity.getViewMode());
1463        final int start = show ? 0 : minHeight;
1464        final int end = show ? minHeight : 0;
1465        ObjectAnimator height = ObjectAnimator.ofInt(this, "animatedHeight", start, end);
1466        height.setInterpolator(new DecelerateInterpolator(2.0f));
1467        height.setDuration(sShrinkAnimationDuration);
1468        return height;
1469    }
1470
1471    // Used by animator
1472    @SuppressWarnings("unused")
1473    public void setItemAlpha(float alpha) {
1474        setAlpha(alpha);
1475        invalidate();
1476    }
1477
1478    // Used by animator
1479    @SuppressWarnings("unused")
1480    public void setAnimatedHeight(int height) {
1481        mAnimatedHeight = height;
1482        requestLayout();
1483    }
1484
1485    @Override
1486    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1487        if (mAnimatedHeight == -1) {
1488            int height = measureHeight(heightMeasureSpec,
1489                    ConversationItemViewCoordinates.getMode(mContext, mActivity.getViewMode()));
1490            setMeasuredDimension(widthMeasureSpec, height);
1491        } else {
1492            setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mAnimatedHeight);
1493        }
1494    }
1495
1496    /**
1497     * Determine the height of this view.
1498     * @param measureSpec A measureSpec packed into an int
1499     * @param mode The current mode of this view
1500     * @return The height of the view, honoring constraints from measureSpec
1501     */
1502    private int measureHeight(int measureSpec, int mode) {
1503        int result = 0;
1504        int specMode = MeasureSpec.getMode(measureSpec);
1505        int specSize = MeasureSpec.getSize(measureSpec);
1506
1507        if (specMode == MeasureSpec.EXACTLY) {
1508            // We were told how big to be
1509            result = specSize;
1510        } else {
1511            // Measure the text
1512            result = ConversationItemViewCoordinates.getHeight(mContext, mode);
1513            if (specMode == MeasureSpec.AT_MOST) {
1514                // Respect AT_MOST value if that was what is called for by
1515                // measureSpec
1516                result = Math.min(result, specSize);
1517            }
1518        }
1519        return result;
1520    }
1521
1522    /**
1523     * Get the current position of this conversation item in the list.
1524     */
1525    public int getPosition() {
1526        return mHeader != null && mHeader.conversation != null ?
1527                mHeader.conversation.position : -1;
1528    }
1529
1530    @Override
1531    public View getSwipeableView() {
1532        return this;
1533    }
1534
1535    /**
1536     * Begin drag mode. Keep the conversation selected (NOT toggle selection) and start drag.
1537     */
1538    private void beginDragMode() {
1539        if (mLastTouchX < 0 || mLastTouchY < 0) {
1540            return;
1541        }
1542        // If this is already checked, don't bother unchecking it!
1543        if (!mChecked) {
1544            toggleCheckMark();
1545        }
1546
1547        // Clip data has form: [conversations_uri, conversationId1,
1548        // maxMessageId1, label1, conversationId2, maxMessageId2, label2, ...]
1549        final int count = mSelectedConversationSet.size();
1550        String description = Utils.formatPlural(mContext, R.plurals.move_conversation, count);
1551
1552        final ClipData data = ClipData.newUri(mContext.getContentResolver(), description,
1553                Conversation.MOVE_CONVERSATIONS_URI);
1554        for (Conversation conversation : mSelectedConversationSet.values()) {
1555            data.addItem(new Item(String.valueOf(conversation.position)));
1556        }
1557        // Protect against non-existent views: only happens for monkeys
1558        final int width = this.getWidth();
1559        final int height = this.getHeight();
1560        final boolean isDimensionNegative = (width < 0) || (height < 0);
1561        if (isDimensionNegative) {
1562            LogUtils.e(LOG_TAG, "ConversationItemView: dimension is negative: "
1563                        + "width=%d, height=%d", width, height);
1564            return;
1565        }
1566        mActivity.startDragMode();
1567        // Start drag mode
1568        startDrag(data, new ShadowBuilder(this, count, mLastTouchX, mLastTouchY), null, 0);
1569    }
1570
1571    /**
1572     * Handles the drag event.
1573     *
1574     * @param event the drag event to be handled
1575     */
1576    @Override
1577    public boolean onDragEvent(DragEvent event) {
1578        switch (event.getAction()) {
1579            case DragEvent.ACTION_DRAG_ENDED:
1580                mActivity.stopDragMode();
1581                return true;
1582        }
1583        return false;
1584    }
1585
1586    private class ShadowBuilder extends DragShadowBuilder {
1587        private final Drawable mBackground;
1588
1589        private final View mView;
1590        private final String mDragDesc;
1591        private final int mTouchX;
1592        private final int mTouchY;
1593        private int mDragDescX;
1594        private int mDragDescY;
1595
1596        public ShadowBuilder(View view, int count, int touchX, int touchY) {
1597            super(view);
1598            mView = view;
1599            mBackground = mView.getResources().getDrawable(R.drawable.list_pressed_holo);
1600            mDragDesc = Utils.formatPlural(mView.getContext(), R.plurals.move_conversation, count);
1601            mTouchX = touchX;
1602            mTouchY = touchY;
1603        }
1604
1605        @Override
1606        public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint) {
1607            int width = mView.getWidth();
1608            int height = mView.getHeight();
1609            mDragDescX = mCoordinates.sendersX;
1610            mDragDescY = getPadding(height, mCoordinates.subjectFontSize)
1611                    - mCoordinates.subjectAscent;
1612            shadowSize.set(width, height);
1613            shadowTouchPoint.set(mTouchX, mTouchY);
1614        }
1615
1616        @Override
1617        public void onDrawShadow(Canvas canvas) {
1618            mBackground.setBounds(0, 0, mView.getWidth(), mView.getHeight());
1619            mBackground.draw(canvas);
1620            sPaint.setTextSize(mCoordinates.subjectFontSize);
1621            canvas.drawText(mDragDesc, mDragDescX, mDragDescY, sPaint);
1622        }
1623    }
1624
1625    @Override
1626    public float getMinAllowScrollDistance() {
1627        return sScrollSlop;
1628    }
1629}
1630