1/*
2 * Copyright (C) 2012 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.mail.browse;
19
20import android.animation.Animator;
21import android.animation.Animator.AnimatorListener;
22import android.animation.AnimatorListenerAdapter;
23import android.animation.AnimatorSet;
24import android.animation.ObjectAnimator;
25import android.content.ClipData;
26import android.content.ClipData.Item;
27import android.content.Context;
28import android.content.res.Resources;
29import android.graphics.Bitmap;
30import android.graphics.BitmapFactory;
31import android.graphics.Canvas;
32import android.graphics.Color;
33import android.graphics.LinearGradient;
34import android.graphics.Matrix;
35import android.graphics.Paint;
36import android.graphics.Point;
37import android.graphics.Rect;
38import android.graphics.Shader;
39import android.graphics.Typeface;
40import android.graphics.drawable.Drawable;
41import android.text.Layout.Alignment;
42import android.text.Spannable;
43import android.text.SpannableString;
44import android.text.SpannableStringBuilder;
45import android.text.StaticLayout;
46import android.text.TextPaint;
47import android.text.TextUtils;
48import android.text.TextUtils.TruncateAt;
49import android.text.format.DateUtils;
50import android.text.style.CharacterStyle;
51import android.text.style.ForegroundColorSpan;
52import android.text.style.TextAppearanceSpan;
53import android.text.util.Rfc822Token;
54import android.text.util.Rfc822Tokenizer;
55import android.util.SparseArray;
56import android.util.TypedValue;
57import android.view.DragEvent;
58import android.view.MotionEvent;
59import android.view.View;
60import android.view.ViewGroup;
61import android.view.ViewParent;
62import android.view.animation.DecelerateInterpolator;
63import android.view.animation.LinearInterpolator;
64import android.widget.AbsListView;
65import android.widget.AbsListView.OnScrollListener;
66import android.widget.TextView;
67
68import com.android.mail.R;
69import com.android.mail.R.drawable;
70import com.android.mail.R.integer;
71import com.android.mail.analytics.Analytics;
72import com.android.mail.bitmap.AttachmentDrawable;
73import com.android.mail.bitmap.AttachmentGridDrawable;
74import com.android.mail.browse.ConversationItemViewModel.SenderFragment;
75import com.android.mail.perf.Timer;
76import com.android.mail.photomanager.ContactPhotoManager;
77import com.android.mail.photomanager.ContactPhotoManager.ContactIdentifier;
78import com.android.mail.photomanager.PhotoManager.PhotoIdentifier;
79import com.android.mail.providers.Address;
80import com.android.mail.providers.Attachment;
81import com.android.mail.providers.Conversation;
82import com.android.mail.providers.Folder;
83import com.android.mail.providers.UIProvider;
84import com.android.mail.providers.UIProvider.AttachmentRendition;
85import com.android.mail.providers.UIProvider.ConversationColumns;
86import com.android.mail.providers.UIProvider.ConversationListIcon;
87import com.android.mail.providers.UIProvider.FolderType;
88import com.android.mail.ui.AnimatedAdapter;
89import com.android.mail.ui.AnimatedAdapter.ConversationListListener;
90import com.android.mail.ui.ControllableActivity;
91import com.android.mail.ui.ConversationSelectionSet;
92import com.android.mail.ui.DividedImageCanvas;
93import com.android.mail.ui.DividedImageCanvas.InvalidateCallback;
94import com.android.mail.ui.FolderDisplayer;
95import com.android.mail.ui.SwipeableItemView;
96import com.android.mail.ui.SwipeableListView;
97import com.android.mail.ui.ViewMode;
98import com.android.mail.utils.FolderUri;
99import com.android.mail.utils.HardwareLayerEnabler;
100import com.android.mail.utils.LogTag;
101import com.android.mail.utils.LogUtils;
102import com.android.mail.utils.Utils;
103import com.google.common.annotations.VisibleForTesting;
104import com.google.common.collect.Lists;
105
106import java.util.ArrayList;
107import java.util.List;
108
109public class ConversationItemView extends View
110        implements SwipeableItemView, ToggleableItem, InvalidateCallback, OnScrollListener {
111
112    // Timer.
113    private static int sLayoutCount = 0;
114    private static Timer sTimer; // Create the sTimer here if you need to do
115                                 // perf analysis.
116    private static final int PERF_LAYOUT_ITERATIONS = 50;
117    private static final String PERF_TAG_LAYOUT = "CCHV.layout";
118    private static final String PERF_TAG_CALCULATE_TEXTS_BITMAPS = "CCHV.txtsbmps";
119    private static final String PERF_TAG_CALCULATE_SENDER_SUBJECT = "CCHV.sendersubj";
120    private static final String PERF_TAG_CALCULATE_FOLDERS = "CCHV.folders";
121    private static final String PERF_TAG_CALCULATE_COORDINATES = "CCHV.coordinates";
122    private static final String LOG_TAG = LogTag.getLogTag();
123
124    // Static bitmaps.
125    private static Bitmap STAR_OFF;
126    private static Bitmap STAR_ON;
127    private static Bitmap CHECK;
128    private static Bitmap ATTACHMENT;
129    private static Bitmap ONLY_TO_ME;
130    private static Bitmap TO_ME_AND_OTHERS;
131    private static Bitmap IMPORTANT_ONLY_TO_ME;
132    private static Bitmap IMPORTANT_TO_ME_AND_OTHERS;
133    private static Bitmap IMPORTANT_TO_OTHERS;
134    private static Bitmap STATE_REPLIED;
135    private static Bitmap STATE_FORWARDED;
136    private static Bitmap STATE_REPLIED_AND_FORWARDED;
137    private static Bitmap STATE_CALENDAR_INVITE;
138    private static Bitmap VISIBLE_CONVERSATION_CARET;
139    private static Drawable RIGHT_EDGE_TABLET;
140    private static Drawable PLACEHOLDER;
141    private static Drawable PROGRESS_BAR;
142
143    private static String sSendersSplitToken;
144    private static String sElidedPaddingToken;
145
146    // Static colors.
147    private static int sSendersTextColorRead;
148    private static int sSendersTextColorUnread;
149    private static int sDateTextColor;
150    private static int sStarTouchSlop;
151    private static int sSenderImageTouchSlop;
152    private static int sShrinkAnimationDuration;
153    private static int sSlideAnimationDuration;
154    private static int sOverflowCountMax;
155    private static int sCabAnimationDuration;
156
157    // Static paints.
158    private static final TextPaint sPaint = new TextPaint();
159    private static final TextPaint sFoldersPaint = new TextPaint();
160    private static final Paint sCheckBackgroundPaint = new Paint();
161
162    // Backgrounds for different states.
163    private final SparseArray<Drawable> mBackgrounds = new SparseArray<Drawable>();
164
165    // Dimensions and coordinates.
166    private int mViewWidth = -1;
167    /** The view mode at which we calculated mViewWidth previously. */
168    private int mPreviousMode;
169
170    private int mInfoIconX;
171    private int mDateX;
172    private int mPaperclipX;
173    private int mSendersWidth;
174
175    /** Whether we are on a tablet device or not */
176    private final boolean mTabletDevice;
177    /** Whether we are on an expansive tablet */
178    private final boolean mIsExpansiveTablet;
179    /** When in conversation mode, true if the list is hidden */
180    private final boolean mListCollapsible;
181
182    @VisibleForTesting
183    ConversationItemViewCoordinates mCoordinates;
184
185    private ConversationItemViewCoordinates.Config mConfig;
186
187    private final Context mContext;
188
189    public ConversationItemViewModel mHeader;
190    private boolean mDownEvent;
191    private boolean mSelected = false;
192    private ConversationSelectionSet mSelectedConversationSet;
193    private Folder mDisplayedFolder;
194    private boolean mStarEnabled;
195    private boolean mSwipeEnabled;
196    private int mLastTouchX;
197    private int mLastTouchY;
198    private AnimatedAdapter mAdapter;
199    private float mAnimatedHeightFraction = 1.0f;
200    private final String mAccount;
201    private ControllableActivity mActivity;
202    private ConversationListListener mConversationListListener;
203    private final TextView mSubjectTextView;
204    private final TextView mSendersTextView;
205    private int mGadgetMode;
206    private boolean mAttachmentPreviewsEnabled;
207    private boolean mParallaxSpeedAlternative;
208    private boolean mParallaxDirectionAlternative;
209    private final DividedImageCanvas mContactImagesHolder;
210    private static ContactPhotoManager sContactPhotoManager;
211
212    private static int sFoldersLeftPadding;
213    private static TextAppearanceSpan sSubjectTextUnreadSpan;
214    private static TextAppearanceSpan sSubjectTextReadSpan;
215    private static ForegroundColorSpan sSnippetTextUnreadSpan;
216    private static ForegroundColorSpan sSnippetTextReadSpan;
217    private static int sScrollSlop;
218    private static CharacterStyle sActivatedTextSpan;
219
220    private final AttachmentGridDrawable mAttachmentsView;
221
222    private final Matrix mPhotoFlipMatrix = new Matrix();
223    private final Matrix mCheckMatrix = new Matrix();
224
225    private final CabAnimator mPhotoFlipAnimator;
226
227    /**
228     * The conversation id, if this conversation was selected the last time we were in a selection
229     * mode. This is reset after any animations complete upon exiting the selection mode.
230     */
231    private long mLastSelectedId = -1;
232
233    /** The resource id of the color to use to override the background. */
234    private int mBackgroundOverrideResId = -1;
235    /** The bitmap to use, or <code>null</code> for the default */
236    private Bitmap mPhotoBitmap = null;
237    private Rect mPhotoRect = null;
238
239    /**
240     * A listener for clicks on the various areas of a conversation item.
241     */
242    public interface ConversationItemAreaClickListener {
243        /** Called when the info icon is clicked. */
244        void onInfoIconClicked();
245
246        /** Called when the star is clicked. */
247        void onStarClicked();
248    }
249
250    /** If set, it will steal all clicks for which the interface has a click method. */
251    private ConversationItemAreaClickListener mConversationItemAreaClickListener = null;
252
253    static {
254        sPaint.setAntiAlias(true);
255        sFoldersPaint.setAntiAlias(true);
256
257        sCheckBackgroundPaint.setColor(Color.GRAY);
258    }
259
260    public static void setScrollStateChanged(final int scrollState) {
261        if (sContactPhotoManager == null) {
262            return;
263        }
264        final boolean flinging = scrollState == OnScrollListener.SCROLL_STATE_FLING;
265
266        if (flinging) {
267            sContactPhotoManager.pause();
268        } else {
269            sContactPhotoManager.resume();
270        }
271    }
272
273    /**
274     * Handles displaying folders in a conversation header view.
275     */
276    static class ConversationItemFolderDisplayer extends FolderDisplayer {
277
278        private int mFoldersCount;
279
280        public ConversationItemFolderDisplayer(Context context) {
281            super(context);
282        }
283
284        @Override
285        public void loadConversationFolders(Conversation conv, final FolderUri ignoreFolderUri,
286                final int ignoreFolderType) {
287            super.loadConversationFolders(conv, ignoreFolderUri, ignoreFolderType);
288            mFoldersCount = mFoldersSortedSet.size();
289        }
290
291        @Override
292        public void reset() {
293            super.reset();
294            mFoldersCount = 0;
295        }
296
297        public boolean hasVisibleFolders() {
298            return mFoldersCount > 0;
299        }
300
301        private int measureFolders(int availableSpace, int cellSize) {
302            int totalWidth = 0;
303            boolean firstTime = true;
304            for (Folder f : mFoldersSortedSet) {
305                final String folderString = f.name;
306                int width = (int) sFoldersPaint.measureText(folderString) + cellSize;
307                if (firstTime) {
308                    firstTime = false;
309                } else {
310                    width += sFoldersLeftPadding;
311                }
312                totalWidth += width;
313                if (totalWidth > availableSpace) {
314                    break;
315                }
316            }
317
318            return totalWidth;
319        }
320
321        public void drawFolders(Canvas canvas, ConversationItemViewCoordinates coordinates) {
322            if (mFoldersCount == 0) {
323                return;
324            }
325            final int xMinStart = coordinates.foldersX;
326            final int xEnd = coordinates.foldersXEnd;
327            final int y = coordinates.foldersY;
328            final int height = coordinates.foldersHeight;
329            int textBottomPadding = coordinates.foldersTextBottomPadding;
330
331            sFoldersPaint.setTextSize(coordinates.foldersFontSize);
332            sFoldersPaint.setTypeface(coordinates.foldersTypeface);
333
334            // Initialize space and cell size based on the current mode.
335            int availableSpace = xEnd - xMinStart;
336            int maxFoldersCount = availableSpace / coordinates.getFolderMinimumWidth();
337            int foldersCount = Math.min(mFoldersCount, maxFoldersCount);
338            int averageWidth = availableSpace / foldersCount;
339            int cellSize = coordinates.getFolderCellWidth();
340
341            // TODO(ath): sFoldersPaint.measureText() is done 3x in this method. stop that.
342            // Extra credit: maybe cache results across items as long as font size doesn't change.
343
344            final int totalWidth = measureFolders(availableSpace, cellSize);
345            int xStart = xEnd - Math.min(availableSpace, totalWidth);
346            final boolean overflow = totalWidth > availableSpace;
347
348            // Second pass to draw folders.
349            int i = 0;
350            for (Folder f : mFoldersSortedSet) {
351                if (availableSpace <= 0) {
352                    break;
353                }
354                final String folderString = f.name;
355                final int fgColor = f.getForegroundColor(mDefaultFgColor);
356                final int bgColor = f.getBackgroundColor(mDefaultBgColor);
357                boolean labelTooLong = false;
358                final int textW = (int) sFoldersPaint.measureText(folderString);
359                int width = textW + cellSize + sFoldersLeftPadding;
360
361                if (overflow && width > averageWidth) {
362                    if (i < foldersCount - 1) {
363                        width = averageWidth;
364                    } else {
365                        // allow the last label to take all remaining space
366                        // (and don't let it make room for padding)
367                        width = availableSpace + sFoldersLeftPadding;
368                    }
369                    labelTooLong = true;
370                }
371
372                // TODO (mindyp): how to we get this?
373                final boolean isMuted = false;
374                // labelValues.folderId ==
375                // sGmail.getFolderMap(mAccount).getFolderIdIgnored();
376
377                // Draw the box.
378                sFoldersPaint.setColor(bgColor);
379                sFoldersPaint.setStyle(Paint.Style.FILL);
380                canvas.drawRect(xStart, y, xStart + width - sFoldersLeftPadding,
381                        y + height, sFoldersPaint);
382
383                // Draw the text.
384                final int padding = cellSize / 2;
385                sFoldersPaint.setColor(fgColor);
386                sFoldersPaint.setStyle(Paint.Style.FILL);
387                if (labelTooLong) {
388                    final int rightBorder = xStart + width - sFoldersLeftPadding - padding;
389                    final Shader shader = new LinearGradient(rightBorder - padding, y, rightBorder,
390                            y, fgColor, Utils.getTransparentColor(fgColor), Shader.TileMode.CLAMP);
391                    sFoldersPaint.setShader(shader);
392                }
393                canvas.drawText(folderString, xStart + padding, y + height - textBottomPadding,
394                        sFoldersPaint);
395                if (labelTooLong) {
396                    sFoldersPaint.setShader(null);
397                }
398
399                availableSpace -= width;
400                xStart += width;
401                i++;
402            }
403        }
404    }
405
406    public ConversationItemView(Context context, String account) {
407        super(context);
408        Utils.traceBeginSection("CIVC constructor");
409        setClickable(true);
410        setLongClickable(true);
411        mContext = context.getApplicationContext();
412        final Resources res = mContext.getResources();
413        mTabletDevice = Utils.useTabletUI(res);
414        mIsExpansiveTablet =
415                mTabletDevice ? res.getBoolean(R.bool.use_expansive_tablet_ui) : false;
416        mListCollapsible = res.getBoolean(R.bool.list_collapsible);
417        mAccount = account;
418
419        if (STAR_OFF == null) {
420            // Initialize static bitmaps.
421            STAR_OFF = BitmapFactory.decodeResource(res, R.drawable.ic_btn_star_off);
422            STAR_ON = BitmapFactory.decodeResource(res, R.drawable.ic_btn_star_on);
423            CHECK = BitmapFactory.decodeResource(res, R.drawable.ic_avatar_check);
424            ATTACHMENT = BitmapFactory.decodeResource(res, R.drawable.ic_attachment_holo_light);
425            ONLY_TO_ME = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_double);
426            TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_single);
427            IMPORTANT_ONLY_TO_ME = BitmapFactory.decodeResource(res,
428                    R.drawable.ic_email_caret_double_important_unread);
429            IMPORTANT_TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res,
430                    R.drawable.ic_email_caret_single_important_unread);
431            IMPORTANT_TO_OTHERS = BitmapFactory.decodeResource(res,
432                    R.drawable.ic_email_caret_none_important_unread);
433            STATE_REPLIED =
434                    BitmapFactory.decodeResource(res, R.drawable.ic_badge_reply_holo_light);
435            STATE_FORWARDED =
436                    BitmapFactory.decodeResource(res, R.drawable.ic_badge_forward_holo_light);
437            STATE_REPLIED_AND_FORWARDED =
438                    BitmapFactory.decodeResource(res, R.drawable.ic_badge_reply_forward_holo_light);
439            STATE_CALENDAR_INVITE =
440                    BitmapFactory.decodeResource(res, R.drawable.ic_badge_invite_holo_light);
441            VISIBLE_CONVERSATION_CARET = BitmapFactory.decodeResource(res, R.drawable.caret_grey);
442            RIGHT_EDGE_TABLET = res.getDrawable(R.drawable.list_edge_tablet);
443            PLACEHOLDER = res.getDrawable(drawable.ic_attachment_load);
444            PROGRESS_BAR = res.getDrawable(drawable.progress_holo);
445
446            // Initialize colors.
447            sActivatedTextSpan = CharacterStyle.wrap(new ForegroundColorSpan(
448                    res.getColor(R.color.senders_text_color_read)));
449            sSendersTextColorRead = res.getColor(R.color.senders_text_color_read);
450            sSendersTextColorUnread = res.getColor(R.color.senders_text_color_unread);
451            sSubjectTextUnreadSpan = new TextAppearanceSpan(mContext,
452                    R.style.SubjectAppearanceUnreadStyle);
453            sSubjectTextReadSpan = new TextAppearanceSpan(mContext,
454                    R.style.SubjectAppearanceReadStyle);
455            sSnippetTextUnreadSpan =
456                    new ForegroundColorSpan(res.getColor(R.color.snippet_text_color_unread));
457            sSnippetTextReadSpan =
458                    new ForegroundColorSpan(res.getColor(R.color.snippet_text_color_read));
459            sDateTextColor = res.getColor(R.color.date_text_color);
460            sStarTouchSlop = res.getDimensionPixelSize(R.dimen.star_touch_slop);
461            sSenderImageTouchSlop = res.getDimensionPixelSize(R.dimen.sender_image_touch_slop);
462            sShrinkAnimationDuration = res.getInteger(R.integer.shrink_animation_duration);
463            sSlideAnimationDuration = res.getInteger(R.integer.slide_animation_duration);
464            // Initialize static color.
465            sSendersSplitToken = res.getString(R.string.senders_split_token);
466            sElidedPaddingToken = res.getString(R.string.elided_padding_token);
467            sScrollSlop = res.getInteger(R.integer.swipeScrollSlop);
468            sFoldersLeftPadding = res.getDimensionPixelOffset(R.dimen.folders_left_padding);
469            sContactPhotoManager = ContactPhotoManager.createContactPhotoManager(context);
470            sOverflowCountMax = res.getInteger(integer.ap_overflow_max_count);
471            sCabAnimationDuration =
472                    res.getInteger(R.integer.conv_item_view_cab_anim_duration);
473        }
474
475        mPhotoFlipAnimator = new CabAnimator("photoFlipFraction", 0, 2,
476                sCabAnimationDuration) {
477            @Override
478            public void invalidateArea() {
479                final int left = mCoordinates.contactImagesX;
480                final int right = left + mContactImagesHolder.getWidth();
481                final int top = mCoordinates.contactImagesY;
482                final int bottom = top + mContactImagesHolder.getHeight();
483                invalidate(left, top, right, bottom);
484            }
485        };
486
487        mSendersTextView = new TextView(mContext);
488        mSendersTextView.setIncludeFontPadding(false);
489
490        mSubjectTextView = new TextView(mContext);
491        mSubjectTextView.setEllipsize(TextUtils.TruncateAt.END);
492        mSubjectTextView.setIncludeFontPadding(false);
493
494        mContactImagesHolder = new DividedImageCanvas(context, new InvalidateCallback() {
495            @Override
496            public void invalidate() {
497                if (mCoordinates == null) {
498                    return;
499                }
500                ConversationItemView.this.invalidate(mCoordinates.contactImagesX,
501                        mCoordinates.contactImagesY,
502                        mCoordinates.contactImagesX + mCoordinates.contactImagesWidth,
503                        mCoordinates.contactImagesY + mCoordinates.contactImagesHeight);
504            }
505        });
506
507        mAttachmentsView = new AttachmentGridDrawable(res, PLACEHOLDER, PROGRESS_BAR);
508        mAttachmentsView.setCallback(this);
509
510        Utils.traceEndSection();
511    }
512
513    public void bind(final Conversation conversation, final ControllableActivity activity,
514            final ConversationListListener conversationListListener,
515            final ConversationSelectionSet set, final Folder folder,
516            final int checkboxOrSenderImage, final boolean showAttachmentPreviews,
517            final boolean parallaxSpeedAlternative, final boolean parallaxDirectionAlternative,
518            final boolean swipeEnabled, final boolean priorityArrowEnabled,
519            final AnimatedAdapter adapter) {
520        Utils.traceBeginSection("CIVC.bind");
521        bind(ConversationItemViewModel.forConversation(mAccount, conversation), activity,
522                conversationListListener, null /* conversationItemAreaClickListener */, set, folder,
523                checkboxOrSenderImage, showAttachmentPreviews, parallaxSpeedAlternative,
524                parallaxDirectionAlternative, swipeEnabled, priorityArrowEnabled, adapter,
525                -1 /* backgroundOverrideResId */,
526                null /* photoBitmap */);
527        Utils.traceEndSection();
528    }
529
530    public void bindAd(final ConversationItemViewModel conversationItemViewModel,
531            final ControllableActivity activity,
532            final ConversationListListener conversationListListener,
533            final ConversationItemAreaClickListener conversationItemAreaClickListener,
534            final Folder folder, final int checkboxOrSenderImage, final AnimatedAdapter adapter,
535            final int backgroundOverrideResId, final Bitmap photoBitmap) {
536        Utils.traceBeginSection("CIVC.bindAd");
537        bind(conversationItemViewModel, activity, conversationListListener,
538                conversationItemAreaClickListener, null /* set */, folder, checkboxOrSenderImage,
539                false /* attachment previews */, false /* parallax */, false /* parallax */,
540                true /* swipeEnabled */, false /* priorityArrowEnabled */, adapter,
541                backgroundOverrideResId, photoBitmap);
542        Utils.traceEndSection();
543    }
544
545    private void bind(final ConversationItemViewModel header, final ControllableActivity activity,
546            final ConversationListListener conversationListListener,
547            final ConversationItemAreaClickListener conversationItemAreaClickListener,
548            final ConversationSelectionSet set, final Folder folder,
549            final int checkboxOrSenderImage, final boolean showAttachmentPreviews,
550            final boolean parallaxSpeedAlternative, final boolean parallaxDirectionAlternative,
551            boolean swipeEnabled, final boolean priorityArrowEnabled, final AnimatedAdapter adapter,
552            final int backgroundOverrideResId, final Bitmap photoBitmap) {
553        mBackgroundOverrideResId = backgroundOverrideResId;
554        mPhotoBitmap = photoBitmap;
555        mConversationItemAreaClickListener = conversationItemAreaClickListener;
556
557        if (mHeader != null) {
558            // If this was previously bound to a different conversation, remove any contact photo
559            // manager requests.
560            if (header.conversation.id != mHeader.conversation.id ||
561                    (mHeader.displayableSenderNames != null && !mHeader.displayableSenderNames
562                    .equals(header.displayableSenderNames))) {
563                ArrayList<String> divisionIds = mContactImagesHolder.getDivisionIds();
564                if (divisionIds != null) {
565                    mContactImagesHolder.reset();
566                    for (int pos = 0; pos < divisionIds.size(); pos++) {
567                        sContactPhotoManager.removePhoto(ContactPhotoManager.generateHash(
568                                mContactImagesHolder, pos, divisionIds.get(pos)));
569                    }
570                }
571            }
572
573            // If this was previously bound to a different conversation,
574            // remove any attachment preview manager requests.
575            if (header.conversation.id != mHeader.conversation.id
576                    || header.conversation.attachmentPreviewsCount
577                            != mHeader.conversation.attachmentPreviewsCount
578                    || !header.conversation.getAttachmentPreviewUris()
579                            .equals(mHeader.conversation.getAttachmentPreviewUris())) {
580
581                // unbind the attachments view (releasing bitmap references)
582                // (this also cancels all async tasks)
583                for (int i = 0, len = mAttachmentsView.getCount(); i < len; i++) {
584                    mAttachmentsView.getOrCreateDrawable(i).unbind();
585                }
586                // reset the grid, as the newly bound item may have a different attachment count
587                mAttachmentsView.setCount(0);
588            }
589
590            if (header.conversation.id != mHeader.conversation.id) {
591                // Stop the photo flip animation
592                mPhotoFlipAnimator.stopAnimation();
593            }
594        }
595        mCoordinates = null;
596        mHeader = header;
597        mActivity = activity;
598        mConversationListListener = conversationListListener;
599        mSelectedConversationSet = set;
600        mDisplayedFolder = folder;
601        mStarEnabled = folder != null && !folder.isTrash();
602        mSwipeEnabled = swipeEnabled;
603        mAdapter = adapter;
604        mAttachmentsView.setBitmapCache(mAdapter.getBitmapCache());
605        mAttachmentsView.setDecodeAggregator(mAdapter.getDecodeAggregator());
606
607        if (checkboxOrSenderImage == ConversationListIcon.SENDER_IMAGE) {
608            mGadgetMode = ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO;
609        } else {
610            mGadgetMode = ConversationItemViewCoordinates.GADGET_NONE;
611        }
612
613        mAttachmentPreviewsEnabled = showAttachmentPreviews;
614        mParallaxSpeedAlternative = parallaxSpeedAlternative;
615        mParallaxDirectionAlternative = parallaxDirectionAlternative;
616
617        // Initialize folder displayer.
618        if (mHeader.folderDisplayer == null) {
619            mHeader.folderDisplayer = new ConversationItemFolderDisplayer(mContext);
620        } else {
621            mHeader.folderDisplayer.reset();
622        }
623
624        final int ignoreFolderType;
625        if (mDisplayedFolder.isInbox()) {
626            ignoreFolderType = FolderType.INBOX;
627        } else {
628            ignoreFolderType = -1;
629        }
630
631        mHeader.folderDisplayer.loadConversationFolders(mHeader.conversation,
632                mDisplayedFolder.folderUri, ignoreFolderType);
633
634        if (mHeader.dateOverrideText == null) {
635            mHeader.dateText = DateUtils.getRelativeTimeSpanString(mContext,
636                    mHeader.conversation.dateMs);
637        } else {
638            mHeader.dateText = mHeader.dateOverrideText;
639        }
640
641        mConfig = new ConversationItemViewCoordinates.Config()
642            .withGadget(mGadgetMode)
643            .withAttachmentPreviews(getAttachmentPreviewsMode());
644        if (header.folderDisplayer.hasVisibleFolders()) {
645            mConfig.showFolders();
646        }
647        if (header.hasBeenForwarded || header.hasBeenRepliedTo || header.isInvite) {
648            mConfig.showReplyState();
649        }
650        if (mHeader.conversation.color != 0) {
651            mConfig.showColorBlock();
652        }
653        // Personal level.
654        mHeader.personalLevelBitmap = null;
655        if (true) { // TODO: hook this up to a setting
656            final int personalLevel = mHeader.conversation.personalLevel;
657            final boolean isImportant =
658                    mHeader.conversation.priority == UIProvider.ConversationPriority.IMPORTANT;
659            final boolean useImportantMarkers = isImportant && priorityArrowEnabled;
660
661            if (personalLevel == UIProvider.ConversationPersonalLevel.ONLY_TO_ME) {
662                mHeader.personalLevelBitmap = useImportantMarkers ? IMPORTANT_ONLY_TO_ME
663                        : ONLY_TO_ME;
664            } else if (personalLevel == UIProvider.ConversationPersonalLevel.TO_ME_AND_OTHERS) {
665                mHeader.personalLevelBitmap = useImportantMarkers ? IMPORTANT_TO_ME_AND_OTHERS
666                        : TO_ME_AND_OTHERS;
667            } else if (useImportantMarkers) {
668                mHeader.personalLevelBitmap = IMPORTANT_TO_OTHERS;
669            }
670        }
671        if (mHeader.personalLevelBitmap != null) {
672            mConfig.showPersonalIndicator();
673        }
674
675        mAttachmentsView.setOverflowText(null);
676
677        setContentDescription();
678        requestLayout();
679    }
680
681    @Override
682    public void invalidateDrawable(Drawable who) {
683        boolean handled = false;
684        if (mCoordinates != null) {
685            if (mAttachmentsView.equals(who)) {
686                final Rect r = new Rect(who.getBounds());
687                r.offset(mCoordinates.attachmentPreviewsX, mCoordinates.attachmentPreviewsY);
688                ConversationItemView.this.invalidate(r.left, r.top, r.right, r.bottom);
689                handled = true;
690            }
691        }
692        if (!handled) {
693            super.invalidateDrawable(who);
694        }
695    }
696
697    /**
698     * Get the Conversation object associated with this view.
699     */
700    public Conversation getConversation() {
701        return mHeader.conversation;
702    }
703
704    private static void startTimer(String tag) {
705        if (sTimer != null) {
706            sTimer.start(tag);
707        }
708    }
709
710    private static void pauseTimer(String tag) {
711        if (sTimer != null) {
712            sTimer.pause(tag);
713        }
714    }
715
716    @Override
717    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
718        Utils.traceBeginSection("CIVC.measure");
719        final int wSize = MeasureSpec.getSize(widthMeasureSpec);
720
721        final int currentMode = mActivity.getViewMode().getMode();
722        if (wSize != mViewWidth || mPreviousMode != currentMode) {
723            mViewWidth = wSize;
724            mPreviousMode = currentMode;
725        }
726        mHeader.viewWidth = mViewWidth;
727
728        mConfig.updateWidth(wSize).setViewMode(currentMode);
729
730        Resources res = getResources();
731        mHeader.standardScaledDimen = res.getDimensionPixelOffset(R.dimen.standard_scaled_dimen);
732
733        mCoordinates = ConversationItemViewCoordinates.forConfig(mContext, mConfig,
734                mAdapter.getCoordinatesCache());
735
736        if (mPhotoBitmap != null) {
737            mPhotoRect = new Rect(0, 0, mCoordinates.contactImagesWidth,
738                    mCoordinates.contactImagesHeight);
739        }
740
741        final int h = (mAnimatedHeightFraction != 1.0f) ?
742                Math.round(mAnimatedHeightFraction * mCoordinates.height) : mCoordinates.height;
743        setMeasuredDimension(mConfig.getWidth(), h);
744        Utils.traceEndSection();
745    }
746
747    @Override
748    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
749        startTimer(PERF_TAG_LAYOUT);
750        Utils.traceBeginSection("CIVC.layout");
751
752        super.onLayout(changed, left, top, right, bottom);
753
754        Utils.traceBeginSection("text and bitmaps");
755        calculateTextsAndBitmaps();
756        Utils.traceEndSection();
757
758        Utils.traceBeginSection("coordinates");
759        calculateCoordinates();
760        Utils.traceEndSection();
761
762        // Subject.
763        createSubject(mHeader.unread);
764
765        if (!mHeader.isLayoutValid()) {
766            setContentDescription();
767        }
768        mHeader.validate();
769
770        pauseTimer(PERF_TAG_LAYOUT);
771        if (sTimer != null && ++sLayoutCount >= PERF_LAYOUT_ITERATIONS) {
772            sTimer.dumpResults();
773            sTimer = new Timer();
774            sLayoutCount = 0;
775        }
776        Utils.traceEndSection();
777    }
778
779    private void setContentDescription() {
780        if (mActivity.isAccessibilityEnabled()) {
781            mHeader.resetContentDescription();
782            setContentDescription(mHeader.getContentDescription(mContext));
783        }
784    }
785
786    @Override
787    public void setBackgroundResource(int resourceId) {
788        Utils.traceBeginSection("set background resource");
789        Drawable drawable = mBackgrounds.get(resourceId);
790        if (drawable == null) {
791            drawable = getResources().getDrawable(resourceId);
792            mBackgrounds.put(resourceId, drawable);
793        }
794        if (getBackground() != drawable) {
795            super.setBackgroundDrawable(drawable);
796        }
797        Utils.traceEndSection();
798    }
799
800    private void calculateTextsAndBitmaps() {
801        startTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
802
803        if (mSelectedConversationSet != null) {
804            mSelected = mSelectedConversationSet.contains(mHeader.conversation);
805        }
806        setSelected(mSelected);
807        mHeader.gadgetMode = mGadgetMode;
808
809        final boolean isUnread = mHeader.unread;
810        updateBackground(isUnread);
811
812        mHeader.sendersDisplayText = new SpannableStringBuilder();
813        mHeader.styledSendersString = null;
814
815        mHeader.hasDraftMessage = mHeader.conversation.numDrafts() > 0;
816
817        // Parse senders fragments.
818        if (mHeader.preserveSendersText) {
819            // This is a special view that doesn't need special sender formatting
820            mHeader.sendersDisplayText = new SpannableStringBuilder(mHeader.sendersText);
821            loadSenderImages();
822        } else if (mHeader.conversation.conversationInfo != null) {
823            // This is Gmail
824            Context context = getContext();
825            mHeader.messageInfoString = SendersView
826                    .createMessageInfo(context, mHeader.conversation, true);
827            int maxChars = ConversationItemViewCoordinates.getSendersLength(context,
828                    mCoordinates.getMode(), mHeader.conversation.hasAttachments);
829            mHeader.displayableSenderEmails = new ArrayList<String>();
830            mHeader.displayableSenderNames = new ArrayList<String>();
831            mHeader.styledSenders = new ArrayList<SpannableString>();
832            SendersView.format(context, mHeader.conversation.conversationInfo,
833                    mHeader.messageInfoString.toString(), maxChars, mHeader.styledSenders,
834                    mHeader.displayableSenderNames, mHeader.displayableSenderEmails, mAccount,
835                    true);
836
837            if (mHeader.displayableSenderEmails.isEmpty() && mHeader.hasDraftMessage) {
838                mHeader.displayableSenderEmails.add(mAccount);
839                mHeader.displayableSenderNames.add(mAccount);
840            }
841
842            // If we have displayable senders, load their thumbnails
843            loadSenderImages();
844        } else {
845            // This is Email
846            SendersView.formatSenders(mHeader, getContext(), true);
847            if (!TextUtils.isEmpty(mHeader.conversation.senders)) {
848                mHeader.displayableSenderEmails = new ArrayList<String>();
849                mHeader.displayableSenderNames = new ArrayList<String>();
850
851                final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(mHeader.conversation.senders);
852                for (int i = 0; i < tokens.length;i++) {
853                    final Rfc822Token token = tokens[i];
854                    final String senderName = Address.decodeAddressName(token.getName());
855                    final String senderAddress = token.getAddress();
856                    mHeader.displayableSenderEmails.add(senderAddress);
857                    mHeader.displayableSenderNames.add(
858                            !TextUtils.isEmpty(senderName) ? senderName : senderAddress);
859                }
860                loadSenderImages();
861            }
862        }
863
864        if (isAttachmentPreviewsEnabled()) {
865            loadAttachmentPreviews();
866        }
867
868        if (mHeader.isLayoutValid()) {
869            pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
870            return;
871        }
872        startTimer(PERF_TAG_CALCULATE_FOLDERS);
873
874
875        pauseTimer(PERF_TAG_CALCULATE_FOLDERS);
876
877        // Paper clip icon.
878        mHeader.paperclip = null;
879        if (mHeader.conversation.hasAttachments) {
880            mHeader.paperclip = ATTACHMENT;
881        }
882
883        startTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT);
884
885        pauseTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT);
886        pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
887    }
888
889    private boolean isAttachmentPreviewsEnabled() {
890        return mAttachmentPreviewsEnabled && !mHeader.conversation.getAttachmentPreviewUris()
891                .isEmpty();
892    }
893
894    private int getOverflowCount() {
895        return mHeader.conversation.attachmentPreviewsCount - mHeader.conversation
896                .getAttachmentPreviewUris().size();
897    }
898
899    private int getAttachmentPreviewsMode() {
900        if (isAttachmentPreviewsEnabled()) {
901            return mHeader.conversation.read
902                    ? ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_READ
903                    : ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_UNREAD;
904        } else {
905            return ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_NONE;
906        }
907    }
908
909    private float getParallaxSpeedMultiplier() {
910        return mParallaxSpeedAlternative
911                ? SwipeableListView.ATTACHMENT_PARALLAX_MULTIPLIER_ALTERNATIVE
912                : SwipeableListView.ATTACHMENT_PARALLAX_MULTIPLIER_NORMAL;
913    }
914
915    // FIXME(ath): maybe move this to bind(). the only dependency on layout is on tile W/H, which
916    // is immutable.
917    private void loadSenderImages() {
918        if (mGadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO
919                && mHeader.displayableSenderEmails != null
920                && mHeader.displayableSenderEmails.size() > 0) {
921            if (mCoordinates.contactImagesWidth <= 0 || mCoordinates.contactImagesHeight <= 0) {
922                LogUtils.w(LOG_TAG,
923                        "Contact image width(%d) or height(%d) is 0 for mode: (%d).",
924                        mCoordinates.contactImagesWidth, mCoordinates.contactImagesHeight,
925                        mCoordinates.getMode());
926                return;
927            }
928
929            int size = mHeader.displayableSenderEmails.size();
930            final List<Object> keys = Lists.newArrayListWithCapacity(size);
931            for (int i = 0; i < DividedImageCanvas.MAX_DIVISIONS && i < size; i++) {
932                keys.add(mHeader.displayableSenderEmails.get(i));
933            }
934
935            mContactImagesHolder.setDimensions(mCoordinates.contactImagesWidth,
936                    mCoordinates.contactImagesHeight);
937            mContactImagesHolder.setDivisionIds(keys);
938            String emailAddress;
939            for (int i = 0; i < DividedImageCanvas.MAX_DIVISIONS && i < size; i++) {
940                emailAddress = mHeader.displayableSenderEmails.get(i);
941                PhotoIdentifier photoIdentifier = new ContactIdentifier(
942                        mHeader.displayableSenderNames.get(i), emailAddress, i);
943                sContactPhotoManager.loadThumbnail(photoIdentifier, mContactImagesHolder);
944            }
945        }
946    }
947
948    private void loadAttachmentPreviews() {
949        if (mCoordinates.attachmentPreviewsWidth <= 0
950                || mCoordinates.attachmentPreviewsHeight <= 0) {
951            LogUtils.w(LOG_TAG,
952                    "Attachment preview width(%d) or height(%d) is 0 for mode: (%d,%d).",
953                    mCoordinates.attachmentPreviewsWidth, mCoordinates.attachmentPreviewsHeight,
954                    mCoordinates.getMode(), getAttachmentPreviewsMode());
955            return;
956        }
957        Utils.traceBeginSection("attachment previews");
958
959        Utils.traceBeginSection("Setup load attachment previews");
960
961        LogUtils.d(LOG_TAG,
962                "loadAttachmentPreviews: Loading attachment previews for conversation %s",
963                mHeader.conversation);
964
965        // Get list of attachments and states from conversation
966        final ArrayList<String> attachmentUris = mHeader.conversation.getAttachmentPreviewUris();
967        final int previewStates = mHeader.conversation.attachmentPreviewStates;
968        final int displayCount = Math.min(
969                attachmentUris.size(), AttachmentGridDrawable.MAX_VISIBLE_ATTACHMENT_COUNT);
970        Utils.traceEndSection();
971
972        mAttachmentsView.setCoordinates(mCoordinates);
973        mAttachmentsView.setCount(displayCount);
974
975        final int decodeHeight;
976        // if parallax is enabled, increase the desired vertical size of attachment bitmaps
977        // so we have extra pixels to scroll within
978        if (SwipeableListView.ENABLE_ATTACHMENT_PARALLAX) {
979            decodeHeight = Math.round(mCoordinates.attachmentPreviewsDecodeHeight
980                    * getParallaxSpeedMultiplier());
981        } else {
982            decodeHeight = mCoordinates.attachmentPreviewsDecodeHeight;
983        }
984
985        // set the bounds before binding inner drawables so they can decode right away
986        // (they need the their bounds set to know whether to decode to 1x1 or 2x1 dimens)
987        mAttachmentsView.setBounds(0, 0, mCoordinates.attachmentPreviewsWidth,
988                mCoordinates.attachmentPreviewsHeight);
989
990        for (int i = 0; i < displayCount; i++) {
991            Utils.traceBeginSection("setup single attachment preview");
992            final String uri = attachmentUris.get(i);
993
994            // Find the rendition to load based on availability.
995            LogUtils.v(LOG_TAG, "loadAttachmentPreviews: state [BEST, SIMPLE] is [%s, %s] for %s ",
996                    Attachment.getPreviewState(previewStates, i, AttachmentRendition.BEST),
997                    Attachment.getPreviewState(previewStates, i, AttachmentRendition.SIMPLE),
998                    uri);
999            int bestAvailableRendition = -1;
1000            // BEST first, else use less preferred renditions
1001            for (final int rendition : AttachmentRendition.PREFERRED_RENDITIONS) {
1002                if (Attachment.getPreviewState(previewStates, i, rendition)) {
1003                    bestAvailableRendition = rendition;
1004                    break;
1005                }
1006            }
1007
1008            LogUtils.d(LOG_TAG,
1009                    "creating/setting drawable region in CIV=%s canvas=%s rend=%s uri=%s",
1010                    this, mAttachmentsView, bestAvailableRendition, uri);
1011            final AttachmentDrawable drawable = mAttachmentsView.getOrCreateDrawable(i);
1012            drawable.setDecodeDimensions(mCoordinates.attachmentPreviewsWidth, decodeHeight);
1013            drawable.setParallaxSpeedMultiplier(getParallaxSpeedMultiplier());
1014            if (bestAvailableRendition != -1) {
1015                drawable.bind(getContext(), uri, bestAvailableRendition);
1016            } else {
1017                drawable.showStaticPlaceholder();
1018            }
1019
1020            Utils.traceEndSection();
1021        }
1022
1023        Utils.traceEndSection();
1024    }
1025
1026    private static int makeExactSpecForSize(int size) {
1027        return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
1028    }
1029
1030    private static void layoutViewExactly(View v, int w, int h) {
1031        v.measure(makeExactSpecForSize(w), makeExactSpecForSize(h));
1032        v.layout(0, 0, w, h);
1033    }
1034
1035    private void layoutSenders() {
1036        if (mHeader.styledSendersString != null) {
1037            if (isActivated() && showActivatedText()) {
1038                mHeader.styledSendersString.setSpan(sActivatedTextSpan, 0,
1039                        mHeader.styledMessageInfoStringOffset, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1040            } else {
1041                mHeader.styledSendersString.removeSpan(sActivatedTextSpan);
1042            }
1043
1044            final int w = mSendersWidth;
1045            final int h = mCoordinates.sendersHeight;
1046            mSendersTextView.setLayoutParams(new ViewGroup.LayoutParams(w, h));
1047            mSendersTextView.setMaxLines(mCoordinates.sendersLineCount);
1048            mSendersTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCoordinates.sendersFontSize);
1049            layoutViewExactly(mSendersTextView, w, h);
1050
1051            mSendersTextView.setText(mHeader.styledSendersString);
1052        }
1053    }
1054
1055    private void createSubject(final boolean isUnread) {
1056        final String subject = filterTag(mHeader.conversation.subject);
1057        final String snippet = mHeader.conversation.getSnippet();
1058        final Spannable displayedStringBuilder = new SpannableString(
1059                Conversation.getSubjectAndSnippetForDisplay(mContext, subject, snippet));
1060
1061        // since spans affect text metrics, add spans to the string before measure/layout or fancy
1062        // ellipsizing
1063        final int subjectTextLength = (subject != null) ? subject.length() : 0;
1064        if (!TextUtils.isEmpty(subject)) {
1065            displayedStringBuilder.setSpan(TextAppearanceSpan.wrap(
1066                    isUnread ? sSubjectTextUnreadSpan : sSubjectTextReadSpan), 0, subjectTextLength,
1067                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1068        }
1069        if (!TextUtils.isEmpty(snippet)) {
1070            final int startOffset = subjectTextLength;
1071            // Start after the end of the subject text; since the subject may be
1072            // "" or null, this could start at the 0th character in the subjectText string
1073            displayedStringBuilder.setSpan(ForegroundColorSpan.wrap(
1074                    isUnread ? sSnippetTextUnreadSpan : sSnippetTextReadSpan), startOffset,
1075                    displayedStringBuilder.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1076        }
1077        if (isActivated() && showActivatedText()) {
1078            displayedStringBuilder.setSpan(sActivatedTextSpan, 0, displayedStringBuilder.length(),
1079                    Spannable.SPAN_INCLUSIVE_INCLUSIVE);
1080        }
1081
1082        final int subjectWidth = mCoordinates.subjectWidth;
1083        final int subjectHeight = mCoordinates.subjectHeight;
1084        mSubjectTextView.setLayoutParams(new ViewGroup.LayoutParams(subjectWidth, subjectHeight));
1085        mSubjectTextView.setMaxLines(mCoordinates.subjectLineCount);
1086        mSubjectTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCoordinates.subjectFontSize);
1087        layoutViewExactly(mSubjectTextView, subjectWidth, subjectHeight);
1088
1089        mSubjectTextView.setText(displayedStringBuilder);
1090    }
1091
1092    private boolean showActivatedText() {
1093        // For activated elements in tablet in conversation mode, we show an activated color, since
1094        // the background is dark blue for activated versus gray for non-activated.
1095        return mTabletDevice && !mListCollapsible;
1096    }
1097
1098    private boolean canFitFragment(int width, int line, int fixedWidth) {
1099        if (line == mCoordinates.sendersLineCount) {
1100            return width + fixedWidth <= mSendersWidth;
1101        } else {
1102            return width <= mSendersWidth;
1103        }
1104    }
1105
1106    private void calculateCoordinates() {
1107        startTimer(PERF_TAG_CALCULATE_COORDINATES);
1108
1109        sPaint.setTextSize(mCoordinates.dateFontSize);
1110        sPaint.setTypeface(Typeface.DEFAULT);
1111
1112        if (mHeader.infoIcon != null) {
1113            mInfoIconX = mCoordinates.infoIconXEnd - mHeader.infoIcon.getWidth();
1114
1115            // If we have an info icon, we start drawing the date text:
1116            // At the end of the date TextView minus the width of the date text
1117            mDateX = mCoordinates.dateXEnd - (int) sPaint.measureText(
1118                    mHeader.dateText != null ? mHeader.dateText.toString() : "");
1119        } else {
1120            // If there is no info icon, we start drawing the date text:
1121            // At the end of the info icon ImageView minus the width of the date text
1122            // We use the info icon ImageView for positioning, since we want the date text to be
1123            // at the right, since there is no info icon
1124            mDateX = mCoordinates.infoIconXEnd - (int) sPaint.measureText(
1125                    mHeader.dateText != null ? mHeader.dateText.toString() : "");
1126        }
1127
1128        mPaperclipX = mDateX - ATTACHMENT.getWidth() - mCoordinates.datePaddingLeft;
1129
1130        if (mCoordinates.isWide()) {
1131            // In wide mode, the end of the senders should align with
1132            // the start of the subject and is based on a max width.
1133            mSendersWidth = mCoordinates.sendersWidth;
1134        } else {
1135            // In normal mode, the width is based on where the date/attachment icon start.
1136            final int dateAttachmentStart;
1137            // Have this end near the paperclip or date, not the folders.
1138            if (mHeader.paperclip != null) {
1139                dateAttachmentStart = mPaperclipX - mCoordinates.paperclipPaddingLeft;
1140            } else {
1141                dateAttachmentStart = mDateX - mCoordinates.datePaddingLeft;
1142            }
1143            mSendersWidth = dateAttachmentStart - mCoordinates.sendersX;
1144        }
1145
1146        // Second pass to layout each fragment.
1147        sPaint.setTextSize(mCoordinates.sendersFontSize);
1148        sPaint.setTypeface(Typeface.DEFAULT);
1149
1150        if (mHeader.styledSenders != null) {
1151            ellipsizeStyledSenders();
1152            layoutSenders();
1153        } else {
1154            // First pass to calculate width of each fragment.
1155            int totalWidth = 0;
1156            int fixedWidth = 0;
1157            for (SenderFragment senderFragment : mHeader.senderFragments) {
1158                CharacterStyle style = senderFragment.style;
1159                int start = senderFragment.start;
1160                int end = senderFragment.end;
1161                style.updateDrawState(sPaint);
1162                senderFragment.width = (int) sPaint.measureText(mHeader.sendersText, start, end);
1163                boolean isFixed = senderFragment.isFixed;
1164                if (isFixed) {
1165                    fixedWidth += senderFragment.width;
1166                }
1167                totalWidth += senderFragment.width;
1168            }
1169
1170            if (mSendersWidth < 0) {
1171                mSendersWidth = 0;
1172            }
1173            totalWidth = ellipsize(fixedWidth);
1174            mHeader.sendersDisplayLayout = new StaticLayout(mHeader.sendersDisplayText, sPaint,
1175                    mSendersWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
1176        }
1177
1178        if (mSendersWidth < 0) {
1179            mSendersWidth = 0;
1180        }
1181
1182        pauseTimer(PERF_TAG_CALCULATE_COORDINATES);
1183    }
1184
1185    // The rules for displaying ellipsized senders are as follows:
1186    // 1) If there is message info (either a COUNT or DRAFT info to display), it MUST be shown
1187    // 2) If senders do not fit, ellipsize the last one that does fit, and stop
1188    // appending new senders
1189    private int ellipsizeStyledSenders() {
1190        SpannableStringBuilder builder = new SpannableStringBuilder();
1191        float totalWidth = 0;
1192        boolean ellipsize = false;
1193        float width;
1194        SpannableStringBuilder messageInfoString =  mHeader.messageInfoString;
1195        if (messageInfoString.length() > 0) {
1196            CharacterStyle[] spans = messageInfoString.getSpans(0, messageInfoString.length(),
1197                    CharacterStyle.class);
1198            // There is only 1 character style span; make sure we apply all the
1199            // styles to the paint object before measuring.
1200            if (spans.length > 0) {
1201                spans[0].updateDrawState(sPaint);
1202            }
1203            // Paint the message info string to see if we lose space.
1204            float messageInfoWidth = sPaint.measureText(messageInfoString.toString());
1205            totalWidth += messageInfoWidth;
1206        }
1207       SpannableString prevSender = null;
1208       SpannableString ellipsizedText;
1209        for (SpannableString sender : mHeader.styledSenders) {
1210            // There may be null sender strings if there were dupes we had to remove.
1211            if (sender == null) {
1212                continue;
1213            }
1214            // No more width available, we'll only show fixed fragments.
1215            if (ellipsize) {
1216                break;
1217            }
1218            CharacterStyle[] spans = sender.getSpans(0, sender.length(), CharacterStyle.class);
1219            // There is only 1 character style span.
1220            if (spans.length > 0) {
1221                spans[0].updateDrawState(sPaint);
1222            }
1223            // If there are already senders present in this string, we need to
1224            // make sure we prepend the dividing token
1225            if (SendersView.sElidedString.equals(sender.toString())) {
1226                prevSender = sender;
1227                sender = copyStyles(spans, sElidedPaddingToken + sender + sElidedPaddingToken);
1228            } else if (builder.length() > 0
1229                    && (prevSender == null || !SendersView.sElidedString.equals(prevSender
1230                            .toString()))) {
1231                prevSender = sender;
1232                sender = copyStyles(spans, sSendersSplitToken + sender);
1233            } else {
1234                prevSender = sender;
1235            }
1236            if (spans.length > 0) {
1237                spans[0].updateDrawState(sPaint);
1238            }
1239            // Measure the width of the current sender and make sure we have space
1240            width = (int) sPaint.measureText(sender.toString());
1241            if (width + totalWidth > mSendersWidth) {
1242                // The text is too long, new line won't help. We have to
1243                // ellipsize text.
1244                ellipsize = true;
1245                width = mSendersWidth - totalWidth; // ellipsis width?
1246                ellipsizedText = copyStyles(spans,
1247                        TextUtils.ellipsize(sender, sPaint, width, TruncateAt.END));
1248                width = (int) sPaint.measureText(ellipsizedText.toString());
1249            } else {
1250                ellipsizedText = null;
1251            }
1252            totalWidth += width;
1253
1254            final CharSequence fragmentDisplayText;
1255            if (ellipsizedText != null) {
1256                fragmentDisplayText = ellipsizedText;
1257            } else {
1258                fragmentDisplayText = sender;
1259            }
1260            builder.append(fragmentDisplayText);
1261        }
1262        mHeader.styledMessageInfoStringOffset = builder.length();
1263        builder.append(messageInfoString);
1264        mHeader.styledSendersString = builder;
1265        return (int)totalWidth;
1266    }
1267
1268    private static SpannableString copyStyles(CharacterStyle[] spans, CharSequence newText) {
1269        SpannableString s = new SpannableString(newText);
1270        if (spans != null && spans.length > 0) {
1271            s.setSpan(spans[0], 0, s.length(), 0);
1272        }
1273        return s;
1274    }
1275
1276    private int ellipsize(int fixedWidth) {
1277        int totalWidth = 0;
1278        int currentLine = 1;
1279        boolean ellipsize = false;
1280        for (SenderFragment senderFragment : mHeader.senderFragments) {
1281            CharacterStyle style = senderFragment.style;
1282            int start = senderFragment.start;
1283            int end = senderFragment.end;
1284            int width = senderFragment.width;
1285            boolean isFixed = senderFragment.isFixed;
1286            style.updateDrawState(sPaint);
1287
1288            // No more width available, we'll only show fixed fragments.
1289            if (ellipsize && !isFixed) {
1290                senderFragment.shouldDisplay = false;
1291                continue;
1292            }
1293
1294            // New line and ellipsize text if needed.
1295            senderFragment.ellipsizedText = null;
1296            if (isFixed) {
1297                fixedWidth -= width;
1298            }
1299            if (!canFitFragment(totalWidth + width, currentLine, fixedWidth)) {
1300                // The text is too long, new line won't help. We have to
1301                // ellipsize text.
1302                if (totalWidth == 0) {
1303                    ellipsize = true;
1304                } else {
1305                    // New line.
1306                    if (currentLine < mCoordinates.sendersLineCount) {
1307                        currentLine++;
1308                        totalWidth = 0;
1309                        // The text is still too long, we have to ellipsize
1310                        // text.
1311                        if (totalWidth + width > mSendersWidth) {
1312                            ellipsize = true;
1313                        }
1314                    } else {
1315                        ellipsize = true;
1316                    }
1317                }
1318
1319                if (ellipsize) {
1320                    width = mSendersWidth - totalWidth;
1321                    // No more new line, we have to reserve width for fixed
1322                    // fragments.
1323                    if (currentLine == mCoordinates.sendersLineCount) {
1324                        width -= fixedWidth;
1325                    }
1326                    senderFragment.ellipsizedText = TextUtils.ellipsize(
1327                            mHeader.sendersText.substring(start, end), sPaint, width,
1328                            TruncateAt.END).toString();
1329                    width = (int) sPaint.measureText(senderFragment.ellipsizedText);
1330                }
1331            }
1332            senderFragment.shouldDisplay = true;
1333            totalWidth += width;
1334
1335            final CharSequence fragmentDisplayText;
1336            if (senderFragment.ellipsizedText != null) {
1337                fragmentDisplayText = senderFragment.ellipsizedText;
1338            } else {
1339                fragmentDisplayText = mHeader.sendersText.substring(start, end);
1340            }
1341            final int spanStart = mHeader.sendersDisplayText.length();
1342            mHeader.sendersDisplayText.append(fragmentDisplayText);
1343            mHeader.sendersDisplayText.setSpan(senderFragment.style, spanStart,
1344                    mHeader.sendersDisplayText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1345        }
1346        return totalWidth;
1347    }
1348
1349    /**
1350     * If the subject contains the tag of a mailing-list (text surrounded with
1351     * []), return the subject with that tag ellipsized, e.g.
1352     * "[android-gmail-team] Hello" -> "[andr...] Hello"
1353     */
1354    private String filterTag(String subject) {
1355        String result = subject;
1356        String formatString = getContext().getResources().getString(R.string.filtered_tag);
1357        if (!TextUtils.isEmpty(subject) && subject.charAt(0) == '[') {
1358            int end = subject.indexOf(']');
1359            if (end > 0) {
1360                String tag = subject.substring(1, end);
1361                result = String.format(formatString, Utils.ellipsize(tag, 7),
1362                        subject.substring(end + 1));
1363            }
1364        }
1365        return result;
1366    }
1367
1368    @Override
1369    public final void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
1370            int totalItemCount) {
1371        if (SwipeableListView.ENABLE_ATTACHMENT_PARALLAX) {
1372            if (mHeader == null || mCoordinates == null || !isAttachmentPreviewsEnabled()) {
1373                return;
1374            }
1375
1376            invalidate(mCoordinates.attachmentPreviewsX, mCoordinates.attachmentPreviewsY,
1377                    mCoordinates.attachmentPreviewsX + mCoordinates.attachmentPreviewsWidth,
1378                    mCoordinates.attachmentPreviewsY + mCoordinates.attachmentPreviewsHeight);
1379        }
1380    }
1381
1382    @Override
1383    public void onScrollStateChanged(AbsListView view, int scrollState) {
1384    }
1385
1386    @Override
1387    protected void onDraw(Canvas canvas) {
1388        Utils.traceBeginSection("CIVC.draw");
1389
1390        // Contact photo
1391        if (mGadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO) {
1392            canvas.save();
1393            drawContactImageArea(canvas);
1394            canvas.restore();
1395        }
1396
1397        // Senders.
1398        boolean isUnread = mHeader.unread;
1399        // Old style senders; apply text colors/ sizes/ styling.
1400        canvas.save();
1401        if (mHeader.sendersDisplayLayout != null) {
1402            sPaint.setTextSize(mCoordinates.sendersFontSize);
1403            sPaint.setTypeface(SendersView.getTypeface(isUnread));
1404            sPaint.setColor(isUnread ? sSendersTextColorUnread : sSendersTextColorRead);
1405            canvas.translate(mCoordinates.sendersX, mCoordinates.sendersY
1406                    + mHeader.sendersDisplayLayout.getTopPadding());
1407            mHeader.sendersDisplayLayout.draw(canvas);
1408        } else {
1409            drawSenders(canvas);
1410        }
1411        canvas.restore();
1412
1413
1414        // Subject.
1415        sPaint.setTypeface(Typeface.DEFAULT);
1416        canvas.save();
1417        drawSubject(canvas);
1418        canvas.restore();
1419
1420        // Folders.
1421        if (mConfig.areFoldersVisible()) {
1422            mHeader.folderDisplayer.drawFolders(canvas, mCoordinates);
1423        }
1424
1425        // If this folder has a color (combined view/Email), show it here
1426        if (mConfig.isColorBlockVisible()) {
1427            sFoldersPaint.setColor(mHeader.conversation.color);
1428            sFoldersPaint.setStyle(Paint.Style.FILL);
1429            canvas.drawRect(mCoordinates.colorBlockX, mCoordinates.colorBlockY,
1430                    mCoordinates.colorBlockX + mCoordinates.colorBlockWidth,
1431                    mCoordinates.colorBlockY + mCoordinates.colorBlockHeight, sFoldersPaint);
1432        }
1433
1434        // Draw the reply state. Draw nothing if neither replied nor forwarded.
1435        if (mConfig.isReplyStateVisible()) {
1436            if (mHeader.hasBeenRepliedTo && mHeader.hasBeenForwarded) {
1437                canvas.drawBitmap(STATE_REPLIED_AND_FORWARDED, mCoordinates.replyStateX,
1438                        mCoordinates.replyStateY, null);
1439            } else if (mHeader.hasBeenRepliedTo) {
1440                canvas.drawBitmap(STATE_REPLIED, mCoordinates.replyStateX,
1441                        mCoordinates.replyStateY, null);
1442            } else if (mHeader.hasBeenForwarded) {
1443                canvas.drawBitmap(STATE_FORWARDED, mCoordinates.replyStateX,
1444                        mCoordinates.replyStateY, null);
1445            } else if (mHeader.isInvite) {
1446                canvas.drawBitmap(STATE_CALENDAR_INVITE, mCoordinates.replyStateX,
1447                        mCoordinates.replyStateY, null);
1448            }
1449        }
1450
1451        if (mConfig.isPersonalIndicatorVisible()) {
1452            canvas.drawBitmap(mHeader.personalLevelBitmap, mCoordinates.personalIndicatorX,
1453                    mCoordinates.personalIndicatorY, null);
1454        }
1455
1456        // Info icon
1457        if (mHeader.infoIcon != null) {
1458            canvas.drawBitmap(mHeader.infoIcon, mInfoIconX, mCoordinates.infoIconY, sPaint);
1459        }
1460
1461        // Date.
1462        sPaint.setTextSize(mCoordinates.dateFontSize);
1463        sPaint.setTypeface(Typeface.DEFAULT);
1464        sPaint.setColor(sDateTextColor);
1465        drawText(canvas, mHeader.dateText, mDateX, mCoordinates.dateYBaseline,
1466                sPaint);
1467
1468        // Paper clip icon.
1469        if (mHeader.paperclip != null) {
1470            canvas.drawBitmap(mHeader.paperclip, mPaperclipX, mCoordinates.paperclipY, sPaint);
1471        }
1472
1473        if (mStarEnabled) {
1474            // Star.
1475            canvas.drawBitmap(getStarBitmap(), mCoordinates.starX, mCoordinates.starY, sPaint);
1476        }
1477
1478        // Attachment previews
1479        if (isAttachmentPreviewsEnabled()) {
1480            canvas.save();
1481            drawAttachmentPreviews(canvas);
1482            canvas.restore();
1483        }
1484
1485        // right-side edge effect when in tablet conversation mode and the list is not collapsed
1486        if (Utils.getDisplayListRightEdgeEffect(mTabletDevice, mListCollapsible,
1487                mConfig.getViewMode())) {
1488            RIGHT_EDGE_TABLET.setBounds(getWidth() - RIGHT_EDGE_TABLET.getIntrinsicWidth(), 0,
1489                    getWidth(), getHeight());
1490            RIGHT_EDGE_TABLET.draw(canvas);
1491
1492            if (isActivated()) {
1493                // draw caret on the right, centered vertically
1494                final int x = getWidth() - VISIBLE_CONVERSATION_CARET.getWidth();
1495                final int y = (getHeight() - VISIBLE_CONVERSATION_CARET.getHeight()) / 2;
1496                canvas.drawBitmap(VISIBLE_CONVERSATION_CARET, x, y, null);
1497            }
1498        }
1499        Utils.traceEndSection();
1500    }
1501
1502    /**
1503     * Draws the contact images or check, in the correct animated state.
1504     */
1505    private void drawContactImageArea(final Canvas canvas) {
1506        if (isSelected()) {
1507            mLastSelectedId = mHeader.conversation.id;
1508
1509            // Since this is selected, we draw the checkbox if the animation is not running, or if
1510            // it's running, and is past the half-way point
1511            if (mPhotoFlipAnimator.getValue() > 1 || !mPhotoFlipAnimator.isStarted()) {
1512                // Flash in the check
1513                drawCheckbox(canvas);
1514            } else {
1515                // Flip out the contact photo
1516                drawContactImages(canvas);
1517            }
1518        } else {
1519            if ((mConversationListListener.isExitingSelectionMode()
1520                    && mLastSelectedId == mHeader.conversation.id)
1521                    || mPhotoFlipAnimator.isStarted()) {
1522                // Animate back to the photo
1523                if (!mPhotoFlipAnimator.isStarted()) {
1524                    mPhotoFlipAnimator.startAnimation(true /* reverse */);
1525                }
1526
1527                if (mPhotoFlipAnimator.getValue() > 1) {
1528                    // Flash out the check
1529                    drawCheckbox(canvas);
1530                } else {
1531                    // Flip in the contact photo
1532                    drawContactImages(canvas);
1533                }
1534            } else {
1535                mLastSelectedId = -1; // We don't care anymore
1536                mPhotoFlipAnimator.stopAnimation(); // It's not running, but we want to reset state
1537
1538                // Contact photos
1539                drawContactImages(canvas);
1540            }
1541        }
1542    }
1543
1544    private void drawContactImages(final Canvas canvas) {
1545        // mPhotoFlipFraction goes from 0 to 1
1546        final float value = mPhotoFlipAnimator.getValue();
1547
1548        final float scale = 1f - value;
1549        final float xOffset = mContactImagesHolder.getWidth() * value / 2;
1550
1551        mPhotoFlipMatrix.reset();
1552        mPhotoFlipMatrix.postScale(scale, 1);
1553
1554        final float x = mCoordinates.contactImagesX + xOffset;
1555        final float y = mCoordinates.contactImagesY;
1556
1557        canvas.translate(x, y);
1558
1559        if (mPhotoBitmap == null) {
1560            mContactImagesHolder.draw(canvas, mPhotoFlipMatrix);
1561        } else {
1562            canvas.drawBitmap(mPhotoBitmap, null, mPhotoRect, sPaint);
1563        }
1564    }
1565
1566    private void drawCheckbox(final Canvas canvas) {
1567        // mPhotoFlipFraction goes from 1 to 2
1568
1569        // Draw the background
1570        canvas.save();
1571        canvas.translate(mCoordinates.contactImagesX, mCoordinates.contactImagesY);
1572        canvas.drawRect(0, 0, mCoordinates.contactImagesWidth, mCoordinates.contactImagesHeight,
1573                sCheckBackgroundPaint);
1574        canvas.restore();
1575
1576        final int x = mCoordinates.contactImagesX
1577                + (mCoordinates.contactImagesWidth - CHECK.getWidth()) / 2;
1578        final int y = mCoordinates.contactImagesY
1579                + (mCoordinates.contactImagesHeight - CHECK.getHeight()) / 2;
1580
1581        final float value = mPhotoFlipAnimator.getValue();
1582        final float scale;
1583
1584        if (!mPhotoFlipAnimator.isStarted()) {
1585            // We're not animating
1586            scale = 1;
1587        } else if (value < 1.9) {
1588            // 1.0 to 1.9 will scale 0 to 1
1589            scale = (value - 1f) / 0.9f;
1590        } else if (value < 1.95) {
1591            // 1.9 to 1.95 will scale 1 to 19/18
1592            scale = (value - 1f) / 0.9f;
1593        } else {
1594            // 1.95 to 2.0 will scale 19/18 to 1
1595            scale = (0.95f - (value - 1.95f)) / 0.9f;
1596        }
1597
1598        final float xOffset = CHECK.getWidth() * (1f - scale) / 2f;
1599        final float yOffset = CHECK.getHeight() * (1f - scale) / 2f;
1600
1601        mCheckMatrix.reset();
1602        mCheckMatrix.postScale(scale, scale);
1603
1604        canvas.translate(x + xOffset, y + yOffset);
1605
1606        canvas.drawBitmap(CHECK, mCheckMatrix, sPaint);
1607    }
1608
1609    private void drawAttachmentPreviews(Canvas canvas) {
1610        canvas.translate(mCoordinates.attachmentPreviewsX, mCoordinates.attachmentPreviewsY);
1611        final float fraction;
1612        if (SwipeableListView.ENABLE_ATTACHMENT_PARALLAX) {
1613            final View listView = getListView();
1614            final View listItemView = unwrap();
1615            if (mParallaxDirectionAlternative) {
1616                fraction = 1 - (float) listItemView.getBottom()
1617                        / (listView.getHeight() + listItemView.getHeight());
1618            } else {
1619                fraction = (float) listItemView.getBottom()
1620                        / (listView.getHeight() + listItemView.getHeight());
1621            }
1622        } else {
1623            // Vertically center the preview crop, which has already been decoded at 1/3.
1624            fraction = 0.5f;
1625        }
1626        mAttachmentsView.setParallaxFraction(fraction);
1627        mAttachmentsView.draw(canvas);
1628    }
1629
1630    private void drawSubject(Canvas canvas) {
1631        canvas.translate(mCoordinates.subjectX, mCoordinates.subjectY);
1632        mSubjectTextView.draw(canvas);
1633    }
1634
1635    private void drawSenders(Canvas canvas) {
1636        canvas.translate(mCoordinates.sendersX, mCoordinates.sendersY);
1637        mSendersTextView.draw(canvas);
1638    }
1639
1640    private Bitmap getStarBitmap() {
1641        return mHeader.conversation.starred ? STAR_ON : STAR_OFF;
1642    }
1643
1644    private static void drawText(Canvas canvas, CharSequence s, int x, int y, TextPaint paint) {
1645        canvas.drawText(s, 0, s.length(), x, y, paint);
1646    }
1647
1648    /**
1649     * Set the background for this item based on:
1650     * 1. Read / Unread (unread messages have a lighter background)
1651     * 2. Tablet / Phone
1652     * 3. Checkbox checked / Unchecked (controls CAB color for item)
1653     * 4. Activated / Not activated (controls the blue highlight on tablet)
1654     * @param isUnread
1655     */
1656    private void updateBackground(boolean isUnread) {
1657        final int background;
1658        if (mBackgroundOverrideResId > 0) {
1659            background = mBackgroundOverrideResId;
1660        } else if (isUnread) {
1661            background = R.drawable.conversation_unread_selector;
1662        } else {
1663            background = R.drawable.conversation_read_selector;
1664        }
1665        setBackgroundResource(background);
1666    }
1667
1668    /**
1669     * Toggle the check mark on this view and update the conversation or begin
1670     * drag, if drag is enabled.
1671     */
1672    @Override
1673    public boolean toggleSelectedStateOrBeginDrag() {
1674        ViewMode mode = mActivity.getViewMode();
1675        if (mIsExpansiveTablet && mode.isListMode()) {
1676            return beginDragMode();
1677        } else {
1678            return toggleSelectedState("long_press");
1679        }
1680    }
1681
1682    @Override
1683    public boolean toggleSelectedState() {
1684        return toggleSelectedState(null);
1685    }
1686
1687    private boolean toggleSelectedState(String sourceOpt) {
1688        if (mHeader != null && mHeader.conversation != null && mSelectedConversationSet != null) {
1689            mSelected = !mSelected;
1690            setSelected(mSelected);
1691            Conversation conv = mHeader.conversation;
1692            // Set the list position of this item in the conversation
1693            SwipeableListView listView = getListView();
1694
1695            try {
1696                conv.position = mSelected && listView != null ? listView.getPositionForView(this)
1697                        : Conversation.NO_POSITION;
1698            } catch (final NullPointerException e) {
1699                // TODO(skennedy) Remove this if we find the root cause b/9527863
1700            }
1701
1702            if (mSelectedConversationSet.isEmpty()) {
1703                final String source = (sourceOpt != null) ? sourceOpt : "checkbox";
1704                Analytics.getInstance().sendEvent("enter_cab_mode", source, null, 0);
1705            }
1706
1707            mSelectedConversationSet.toggle(conv);
1708            if (mSelectedConversationSet.isEmpty()) {
1709                listView.commitDestructiveActions(true);
1710            }
1711
1712            final boolean reverse = !mSelected;
1713            mPhotoFlipAnimator.startAnimation(reverse);
1714            mPhotoFlipAnimator.invalidateArea();
1715
1716            // We update the background after the checked state has changed
1717            // now that we have a selected background asset. Setting the background
1718            // usually waits for a layout pass, but we don't need a full layout,
1719            // just an update to the background.
1720            requestLayout();
1721
1722            return true;
1723        }
1724
1725        return false;
1726    }
1727
1728    /**
1729     * Toggle the star on this view and update the conversation.
1730     */
1731    public void toggleStar() {
1732        mHeader.conversation.starred = !mHeader.conversation.starred;
1733        Bitmap starBitmap = getStarBitmap();
1734        postInvalidate(mCoordinates.starX, mCoordinates.starY, mCoordinates.starX
1735                + starBitmap.getWidth(),
1736                mCoordinates.starY + starBitmap.getHeight());
1737        ConversationCursor cursor = (ConversationCursor) mAdapter.getCursor();
1738        if (cursor != null) {
1739            // TODO(skennedy) What about ads?
1740            cursor.updateBoolean(mHeader.conversation, ConversationColumns.STARRED,
1741                    mHeader.conversation.starred);
1742        }
1743    }
1744
1745    private boolean isTouchInContactPhoto(float x, float y) {
1746        // Everything before the right edge of contact photo
1747
1748        final int threshold = mCoordinates.contactImagesX + mCoordinates.contactImagesWidth
1749                + sSenderImageTouchSlop;
1750
1751        // Allow touching a little right of the contact photo when we're already in selection mode
1752        final float extra;
1753        if (mSelectedConversationSet == null || mSelectedConversationSet.isEmpty()) {
1754            extra = 0;
1755        } else {
1756            extra = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16,
1757                    getResources().getDisplayMetrics());
1758        }
1759
1760        return mHeader.gadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO
1761                && x < (threshold + extra)
1762                && (!isAttachmentPreviewsEnabled() || y < mCoordinates.attachmentPreviewsY);
1763    }
1764
1765    private boolean isTouchInInfoIcon(final float x, final float y) {
1766        if (mHeader.infoIcon == null) {
1767            // We have no info icon
1768            return false;
1769        }
1770
1771        // Regardless of device, we always want to be right of the date's left touch slop
1772        if (x < mDateX - sStarTouchSlop) {
1773            return false;
1774        }
1775
1776        if (mStarEnabled) {
1777            if (mIsExpansiveTablet) {
1778                // Just check that we're left of the star's touch area
1779                if (x >= mCoordinates.starX - sStarTouchSlop) {
1780                    return false;
1781                }
1782            } else {
1783                // We're on a phone or non-expansive tablet
1784
1785                // We allow touches all the way to the right edge, so no x check is necessary
1786
1787                // We need to be above the star's touch area, which ends at the top of the subject
1788                // text
1789                return y < mCoordinates.subjectY;
1790            }
1791        }
1792
1793        // With no star below the info icon, we allow touches anywhere from the top edge to the
1794        // bottom edge, or to the top of the attachment previews, whichever is higher
1795        return !isAttachmentPreviewsEnabled() || y < mCoordinates.attachmentPreviewsY;
1796    }
1797
1798    private boolean isTouchInStar(float x, float y) {
1799        if (mHeader.infoIcon != null && !mIsExpansiveTablet) {
1800            // We have an info icon, and it's above the star
1801            // We allow touches everywhere below the top of the subject text
1802            if (y < mCoordinates.subjectY) {
1803                return false;
1804            }
1805        }
1806
1807        // Everything after the star and include a touch slop.
1808        return mStarEnabled
1809                && x > mCoordinates.starX - sStarTouchSlop
1810                && (!isAttachmentPreviewsEnabled() || y < mCoordinates.attachmentPreviewsY);
1811    }
1812
1813    @Override
1814    public boolean canChildBeDismissed() {
1815        return true;
1816    }
1817
1818    @Override
1819    public void dismiss() {
1820        SwipeableListView listView = getListView();
1821        if (listView != null) {
1822            getListView().dismissChild(this);
1823        }
1824    }
1825
1826    private boolean onTouchEventNoSwipe(MotionEvent event) {
1827        Utils.traceBeginSection("on touch event no swipe");
1828        boolean handled = false;
1829
1830        int x = (int) event.getX();
1831        int y = (int) event.getY();
1832        mLastTouchX = x;
1833        mLastTouchY = y;
1834        switch (event.getAction()) {
1835            case MotionEvent.ACTION_DOWN:
1836                if (isTouchInContactPhoto(x, y) || isTouchInInfoIcon(x, y) || isTouchInStar(x, y)) {
1837                    mDownEvent = true;
1838                    handled = true;
1839                }
1840                break;
1841
1842            case MotionEvent.ACTION_CANCEL:
1843                mDownEvent = false;
1844                break;
1845
1846            case MotionEvent.ACTION_UP:
1847                if (mDownEvent) {
1848                    if (isTouchInContactPhoto(x, y)) {
1849                        // Touch on the check mark
1850                        toggleSelectedState();
1851                    } else if (isTouchInInfoIcon(x, y)) {
1852                        if (mConversationItemAreaClickListener != null) {
1853                            mConversationItemAreaClickListener.onInfoIconClicked();
1854                        }
1855                    } else if (isTouchInStar(x, y)) {
1856                        // Touch on the star
1857                        if (mConversationItemAreaClickListener == null) {
1858                            toggleStar();
1859                        } else {
1860                            mConversationItemAreaClickListener.onStarClicked();
1861                        }
1862                    }
1863                    handled = true;
1864                }
1865                break;
1866        }
1867
1868        if (!handled) {
1869            handled = super.onTouchEvent(event);
1870        }
1871
1872        Utils.traceEndSection();
1873        return handled;
1874    }
1875
1876    /**
1877     * ConversationItemView is given the first chance to handle touch events.
1878     */
1879    @Override
1880    public boolean onTouchEvent(MotionEvent event) {
1881        Utils.traceBeginSection("on touch event");
1882        int x = (int) event.getX();
1883        int y = (int) event.getY();
1884        mLastTouchX = x;
1885        mLastTouchY = y;
1886        if (!mSwipeEnabled) {
1887            Utils.traceEndSection();
1888            return onTouchEventNoSwipe(event);
1889        }
1890        switch (event.getAction()) {
1891            case MotionEvent.ACTION_DOWN:
1892                if (isTouchInContactPhoto(x, y) || isTouchInInfoIcon(x, y) || isTouchInStar(x, y)) {
1893                    mDownEvent = true;
1894                    Utils.traceEndSection();
1895                    return true;
1896                }
1897                break;
1898            case MotionEvent.ACTION_UP:
1899                if (mDownEvent) {
1900                    if (isTouchInContactPhoto(x, y)) {
1901                        // Touch on the check mark
1902                        Utils.traceEndSection();
1903                        mDownEvent = false;
1904                        toggleSelectedState();
1905                        Utils.traceEndSection();
1906                        return true;
1907                    } else if (isTouchInInfoIcon(x, y)) {
1908                        // Touch on the info icon
1909                        mDownEvent = false;
1910                        if (mConversationItemAreaClickListener != null) {
1911                            mConversationItemAreaClickListener.onInfoIconClicked();
1912                        }
1913                        Utils.traceEndSection();
1914                        return true;
1915                    } else if (isTouchInStar(x, y)) {
1916                        // Touch on the star
1917                        mDownEvent = false;
1918                        if (mConversationItemAreaClickListener == null) {
1919                            toggleStar();
1920                        } else {
1921                            mConversationItemAreaClickListener.onStarClicked();
1922                        }
1923                        Utils.traceEndSection();
1924                        return true;
1925                    }
1926                }
1927                break;
1928        }
1929        // Let View try to handle it as well.
1930        boolean handled = super.onTouchEvent(event);
1931        if (event.getAction() == MotionEvent.ACTION_DOWN) {
1932            Utils.traceEndSection();
1933            return true;
1934        }
1935        Utils.traceEndSection();
1936        return handled;
1937    }
1938
1939    @Override
1940    public boolean performClick() {
1941        final boolean handled = super.performClick();
1942        final SwipeableListView list = getListView();
1943        if (!handled && list != null && list.getAdapter() != null) {
1944            final int pos = list.findConversation(this, mHeader.conversation);
1945            list.performItemClick(this, pos, mHeader.conversation.id);
1946        }
1947        return handled;
1948    }
1949
1950    private View unwrap() {
1951        final ViewParent vp = getParent();
1952        if (vp == null || !(vp instanceof View)) {
1953            return null;
1954        }
1955        return (View) vp;
1956    }
1957
1958    private SwipeableListView getListView() {
1959        SwipeableListView v = null;
1960        final View wrapper = unwrap();
1961        if (wrapper != null && wrapper instanceof SwipeableConversationItemView) {
1962            v = (SwipeableListView) ((SwipeableConversationItemView) wrapper).getListView();
1963        }
1964        if (v == null) {
1965            v = mAdapter.getListView();
1966        }
1967        return v;
1968    }
1969
1970    /**
1971     * Reset any state associated with this conversation item view so that it
1972     * can be reused.
1973     */
1974    public void reset() {
1975        Utils.traceBeginSection("reset");
1976        setAlpha(1f);
1977        setTranslationX(0f);
1978        mAnimatedHeightFraction = 1.0f;
1979        Utils.traceEndSection();
1980    }
1981
1982    @SuppressWarnings("deprecation")
1983    @Override
1984    public void setTranslationX(float translationX) {
1985        super.setTranslationX(translationX);
1986
1987        // When a list item is being swiped or animated, ensure that the hosting view has a
1988        // background color set. We only enable the background during the X-translation effect to
1989        // reduce overdraw during normal list scrolling.
1990        final View parent = (View) getParent();
1991        if (parent == null) {
1992            LogUtils.w(LOG_TAG, "CIV.setTranslationX null ConversationItemView parent x=%s",
1993                    translationX);
1994        }
1995
1996        if (parent instanceof SwipeableConversationItemView) {
1997            if (translationX != 0f) {
1998                parent.setBackgroundResource(R.color.swiped_bg_color);
1999            } else {
2000                parent.setBackgroundDrawable(null);
2001            }
2002        }
2003    }
2004
2005    /**
2006     * Grow the height of the item and fade it in when bringing a conversation
2007     * back from a destructive action.
2008     */
2009    public Animator createSwipeUndoAnimation() {
2010        ObjectAnimator undoAnimator = createTranslateXAnimation(true);
2011        return undoAnimator;
2012    }
2013
2014    /**
2015     * Grow the height of the item and fade it in when bringing a conversation
2016     * back from a destructive action.
2017     */
2018    public Animator createUndoAnimation() {
2019        ObjectAnimator height = createHeightAnimation(true);
2020        Animator fade = ObjectAnimator.ofFloat(this, "alpha", 0, 1.0f);
2021        fade.setDuration(sShrinkAnimationDuration);
2022        fade.setInterpolator(new DecelerateInterpolator(2.0f));
2023        AnimatorSet transitionSet = new AnimatorSet();
2024        transitionSet.playTogether(height, fade);
2025        transitionSet.addListener(new HardwareLayerEnabler(this));
2026        return transitionSet;
2027    }
2028
2029    /**
2030     * Grow the height of the item and fade it in when bringing a conversation
2031     * back from a destructive action.
2032     */
2033    public Animator createDestroyWithSwipeAnimation() {
2034        ObjectAnimator slide = createTranslateXAnimation(false);
2035        ObjectAnimator height = createHeightAnimation(false);
2036        AnimatorSet transitionSet = new AnimatorSet();
2037        transitionSet.playSequentially(slide, height);
2038        return transitionSet;
2039    }
2040
2041    private ObjectAnimator createTranslateXAnimation(boolean show) {
2042        SwipeableListView parent = getListView();
2043        // If we can't get the parent...we have bigger problems.
2044        int width = parent != null ? parent.getMeasuredWidth() : 0;
2045        final float start = show ? width : 0f;
2046        final float end = show ? 0f : width;
2047        ObjectAnimator slide = ObjectAnimator.ofFloat(this, "translationX", start, end);
2048        slide.setInterpolator(new DecelerateInterpolator(2.0f));
2049        slide.setDuration(sSlideAnimationDuration);
2050        return slide;
2051    }
2052
2053    public Animator createDestroyAnimation() {
2054        return createHeightAnimation(false);
2055    }
2056
2057    private ObjectAnimator createHeightAnimation(boolean show) {
2058        final float start = show ? 0f : 1.0f;
2059        final float end = show ? 1.0f : 0f;
2060        ObjectAnimator height = ObjectAnimator.ofFloat(this, "animatedHeightFraction", start, end);
2061        height.setInterpolator(new DecelerateInterpolator(2.0f));
2062        height.setDuration(sShrinkAnimationDuration);
2063        return height;
2064    }
2065
2066    // Used by animator
2067    public void setAnimatedHeightFraction(float height) {
2068        mAnimatedHeightFraction = height;
2069        requestLayout();
2070    }
2071
2072    @Override
2073    public SwipeableView getSwipeableView() {
2074        return SwipeableView.from(this);
2075    }
2076
2077    /**
2078     * Begin drag mode. Keep the conversation selected (NOT toggle selection) and start drag.
2079     */
2080    private boolean beginDragMode() {
2081        if (mLastTouchX < 0 || mLastTouchY < 0 ||  mSelectedConversationSet == null) {
2082            return false;
2083        }
2084        // If this is already checked, don't bother unchecking it!
2085        if (!mSelected) {
2086            toggleSelectedState();
2087        }
2088
2089        // Clip data has form: [conversations_uri, conversationId1,
2090        // maxMessageId1, label1, conversationId2, maxMessageId2, label2, ...]
2091        final int count = mSelectedConversationSet.size();
2092        String description = Utils.formatPlural(mContext, R.plurals.move_conversation, count);
2093
2094        final ClipData data = ClipData.newUri(mContext.getContentResolver(), description,
2095                Conversation.MOVE_CONVERSATIONS_URI);
2096        for (Conversation conversation : mSelectedConversationSet.values()) {
2097            data.addItem(new Item(String.valueOf(conversation.position)));
2098        }
2099        // Protect against non-existent views: only happens for monkeys
2100        final int width = this.getWidth();
2101        final int height = this.getHeight();
2102        final boolean isDimensionNegative = (width < 0) || (height < 0);
2103        if (isDimensionNegative) {
2104            LogUtils.e(LOG_TAG, "ConversationItemView: dimension is negative: "
2105                        + "width=%d, height=%d", width, height);
2106            return false;
2107        }
2108        mActivity.startDragMode();
2109        // Start drag mode
2110        startDrag(data, new ShadowBuilder(this, count, mLastTouchX, mLastTouchY), null, 0);
2111
2112        return true;
2113    }
2114
2115    /**
2116     * Handles the drag event.
2117     *
2118     * @param event the drag event to be handled
2119     */
2120    @Override
2121    public boolean onDragEvent(DragEvent event) {
2122        switch (event.getAction()) {
2123            case DragEvent.ACTION_DRAG_ENDED:
2124                mActivity.stopDragMode();
2125                return true;
2126        }
2127        return false;
2128    }
2129
2130    private class ShadowBuilder extends DragShadowBuilder {
2131        private final Drawable mBackground;
2132
2133        private final View mView;
2134        private final String mDragDesc;
2135        private final int mTouchX;
2136        private final int mTouchY;
2137        private int mDragDescX;
2138        private int mDragDescY;
2139
2140        public ShadowBuilder(View view, int count, int touchX, int touchY) {
2141            super(view);
2142            mView = view;
2143            mBackground = mView.getResources().getDrawable(R.drawable.list_pressed_holo);
2144            mDragDesc = Utils.formatPlural(mView.getContext(), R.plurals.move_conversation, count);
2145            mTouchX = touchX;
2146            mTouchY = touchY;
2147        }
2148
2149        @Override
2150        public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint) {
2151            final int width = mView.getWidth();
2152            final int height = mView.getHeight();
2153
2154            sPaint.setTextSize(mCoordinates.subjectFontSize);
2155            mDragDescX = mCoordinates.sendersX;
2156            mDragDescY = (height - (int) mCoordinates.subjectFontSize) / 2 ;
2157            shadowSize.set(width, height);
2158            shadowTouchPoint.set(mTouchX, mTouchY);
2159        }
2160
2161        @Override
2162        public void onDrawShadow(Canvas canvas) {
2163            mBackground.setBounds(0, 0, mView.getWidth(), mView.getHeight());
2164            mBackground.draw(canvas);
2165            sPaint.setTextSize(mCoordinates.subjectFontSize);
2166            canvas.drawText(mDragDesc, mDragDescX, mDragDescY - sPaint.ascent(), sPaint);
2167        }
2168    }
2169
2170    @Override
2171    public float getMinAllowScrollDistance() {
2172        return sScrollSlop;
2173    }
2174
2175    private abstract class CabAnimator {
2176        private ObjectAnimator mAnimator = null;
2177
2178        private final String mPropertyName;
2179
2180        private float mValue;
2181
2182        private final float mStartValue;
2183        private final float mEndValue;
2184
2185        private final long mDuration;
2186
2187        private boolean mReversing = false;
2188
2189        public CabAnimator(final String propertyName, final float startValue, final float endValue,
2190                final long duration) {
2191            mPropertyName = propertyName;
2192
2193            mStartValue = startValue;
2194            mEndValue = endValue;
2195
2196            mDuration = duration;
2197        }
2198
2199        private ObjectAnimator createAnimator() {
2200            final ObjectAnimator animator = ObjectAnimator.ofFloat(ConversationItemView.this,
2201                    mPropertyName, mStartValue, mEndValue);
2202            animator.setDuration(mDuration);
2203            animator.setInterpolator(new LinearInterpolator());
2204            animator.addListener(new AnimatorListenerAdapter() {
2205                @Override
2206                public void onAnimationEnd(final Animator animation) {
2207                    invalidateArea();
2208                }
2209            });
2210            animator.addListener(mAnimatorListener);
2211            return animator;
2212        }
2213
2214        private final AnimatorListener mAnimatorListener = new AnimatorListener() {
2215            @Override
2216            public void onAnimationStart(final Animator animation) {
2217                // Do nothing
2218            }
2219
2220            @Override
2221            public void onAnimationEnd(final Animator animation) {
2222                if (mReversing) {
2223                    mReversing = false;
2224                    // We no longer want to track whether we were last selected,
2225                    // since we no longer are selected
2226                    mLastSelectedId = -1;
2227                }
2228            }
2229
2230            @Override
2231            public void onAnimationCancel(final Animator animation) {
2232                // Do nothing
2233            }
2234
2235            @Override
2236            public void onAnimationRepeat(final Animator animation) {
2237                // Do nothing
2238            }
2239        };
2240
2241        public abstract void invalidateArea();
2242
2243        public void setValue(final float fraction) {
2244            if (mValue == fraction) {
2245                return;
2246            }
2247            mValue = fraction;
2248            invalidateArea();
2249        }
2250
2251        public float getValue() {
2252            return mValue;
2253        }
2254
2255        /**
2256         * @param reverse <code>true</code> to animate in reverse
2257         */
2258        public void startAnimation(final boolean reverse) {
2259            if (mAnimator != null) {
2260                mAnimator.cancel();
2261            }
2262
2263            mAnimator = createAnimator();
2264            mReversing = reverse;
2265
2266            if (reverse) {
2267                mAnimator.reverse();
2268            } else {
2269                mAnimator.start();
2270            }
2271        }
2272
2273        public void stopAnimation() {
2274            if (mAnimator != null) {
2275                mAnimator.cancel();
2276                mAnimator = null;
2277            }
2278
2279            mReversing = false;
2280
2281            setValue(0);
2282        }
2283
2284        public boolean isStarted() {
2285            return mAnimator != null && mAnimator.isStarted();
2286        }
2287    }
2288
2289    public void setPhotoFlipFraction(final float fraction) {
2290        mPhotoFlipAnimator.setValue(fraction);
2291    }
2292
2293    public String getAccount() {
2294        return mAccount;
2295    }
2296}
2297