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