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