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