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