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