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