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