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