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