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