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