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