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.AnimatorSet;
22import android.animation.ObjectAnimator;
23import android.content.BroadcastReceiver;
24import android.content.ClipData;
25import android.content.ClipData.Item;
26import android.content.Context;
27import android.content.Intent;
28import android.content.IntentFilter;
29import android.content.res.Resources;
30import android.graphics.Bitmap;
31import android.graphics.BitmapFactory;
32import android.graphics.Canvas;
33import android.graphics.Color;
34import android.graphics.LinearGradient;
35import android.graphics.Paint;
36import android.graphics.Point;
37import android.graphics.Rect;
38import android.graphics.RectF;
39import android.graphics.Shader;
40import android.graphics.Typeface;
41import android.graphics.drawable.Drawable;
42import android.graphics.drawable.InsetDrawable;
43import android.support.v4.text.TextUtilsCompat;
44import android.support.v4.view.ViewCompat;
45import android.text.Layout.Alignment;
46import android.text.Spannable;
47import android.text.SpannableString;
48import android.text.SpannableStringBuilder;
49import android.text.StaticLayout;
50import android.text.TextPaint;
51import android.text.TextUtils;
52import android.text.TextUtils.TruncateAt;
53import android.text.format.DateUtils;
54import android.text.style.BackgroundColorSpan;
55import android.text.style.CharacterStyle;
56import android.text.style.ForegroundColorSpan;
57import android.text.style.TextAppearanceSpan;
58import android.util.SparseArray;
59import android.util.TypedValue;
60import android.view.DragEvent;
61import android.view.MotionEvent;
62import android.view.View;
63import android.view.ViewGroup;
64import android.view.ViewParent;
65import android.view.animation.DecelerateInterpolator;
66import android.widget.TextView;
67
68import com.android.mail.R;
69import com.android.mail.analytics.Analytics;
70import com.android.mail.bitmap.CheckableContactFlipDrawable;
71import com.android.mail.bitmap.ContactDrawable;
72import com.android.mail.perf.Timer;
73import com.android.mail.providers.Conversation;
74import com.android.mail.providers.Folder;
75import com.android.mail.providers.UIProvider;
76import com.android.mail.providers.UIProvider.ConversationColumns;
77import com.android.mail.providers.UIProvider.ConversationListIcon;
78import com.android.mail.providers.UIProvider.FolderType;
79import com.android.mail.ui.AnimatedAdapter;
80import com.android.mail.ui.ControllableActivity;
81import com.android.mail.ui.ConversationSelectionSet;
82import com.android.mail.ui.ConversationSetObserver;
83import com.android.mail.ui.DividedImageCanvas.InvalidateCallback;
84import com.android.mail.ui.FolderDisplayer;
85import com.android.mail.ui.SwipeableItemView;
86import com.android.mail.ui.SwipeableListView;
87import com.android.mail.ui.ViewMode;
88import com.android.mail.utils.FolderUri;
89import com.android.mail.utils.HardwareLayerEnabler;
90import com.android.mail.utils.LogTag;
91import com.android.mail.utils.LogUtils;
92import com.android.mail.utils.Utils;
93import com.android.mail.utils.ViewUtils;
94import com.google.common.annotations.VisibleForTesting;
95
96import java.util.ArrayList;
97import java.util.List;
98import java.util.Locale;
99
100public class ConversationItemView extends View
101        implements SwipeableItemView, ToggleableItem, InvalidateCallback, ConversationSetObserver,
102        BadgeSpan.BadgeSpanDimensions {
103
104    // Timer.
105    private static int sLayoutCount = 0;
106    private static Timer sTimer; // Create the sTimer here if you need to do
107                                 // perf analysis.
108    private static final int PERF_LAYOUT_ITERATIONS = 50;
109    private static final String PERF_TAG_LAYOUT = "CCHV.layout";
110    private static final String PERF_TAG_CALCULATE_TEXTS_BITMAPS = "CCHV.txtsbmps";
111    private static final String PERF_TAG_CALCULATE_SENDER_SUBJECT = "CCHV.sendersubj";
112    private static final String PERF_TAG_CALCULATE_FOLDERS = "CCHV.folders";
113    private static final String PERF_TAG_CALCULATE_COORDINATES = "CCHV.coordinates";
114    private static final String LOG_TAG = LogTag.getLogTag();
115
116    private static final Typeface SANS_SERIF_LIGHT = Typeface.create("sans-serif-light",
117            Typeface.NORMAL);
118
119    // Static bitmaps.
120    private static Bitmap STAR_OFF;
121    private static Bitmap STAR_ON;
122    private static Bitmap ATTACHMENT;
123    private static Bitmap ONLY_TO_ME;
124    private static Bitmap TO_ME_AND_OTHERS;
125    private static Bitmap IMPORTANT_ONLY_TO_ME;
126    private static Bitmap IMPORTANT_TO_ME_AND_OTHERS;
127    private static Bitmap IMPORTANT;
128    private static Bitmap STATE_REPLIED;
129    private static Bitmap STATE_FORWARDED;
130    private static Bitmap STATE_REPLIED_AND_FORWARDED;
131    private static Bitmap STATE_CALENDAR_INVITE;
132    private static Drawable VISIBLE_CONVERSATION_HIGHLIGHT;
133    private static Drawable RIGHT_EDGE_TABLET;
134
135    private static String sSendersSplitToken;
136    private static String sElidedPaddingToken;
137
138    // Static colors.
139    private static int sSendersTextColor;
140    private static int sDateTextColorRead;
141    private static int sDateTextColorUnread;
142    private static int sStarTouchSlop;
143    private static int sSenderImageTouchSlop;
144    private static int sShrinkAnimationDuration;
145    private static int sSlideAnimationDuration;
146    private static int sCabAnimationDuration;
147    private static int sBadgePaddingExtraWidth;
148    private static int sBadgeRoundedCornerRadius;
149    private static int sFolderRoundedCornerRadius;
150    private static int sDividerColor;
151
152    // Static paints.
153    private static final TextPaint sPaint = new TextPaint();
154    private static final TextPaint sFoldersPaint = new TextPaint();
155    private static final Paint sCheckBackgroundPaint = new Paint();
156    private static final Paint sDividerPaint = new Paint();
157
158    private static int sDividerInset;
159    private static int sDividerHeight;
160
161    private static BroadcastReceiver sConfigurationChangedReceiver;
162
163    // Backgrounds for different states.
164    private final SparseArray<Drawable> mBackgrounds = new SparseArray<Drawable>();
165
166    // Dimensions and coordinates.
167    private int mViewWidth = -1;
168    /** The view mode at which we calculated mViewWidth previously. */
169    private int mPreviousMode;
170
171    private int mInfoIconX;
172    private int mDateX;
173    private int mDateWidth;
174    private int mPaperclipX;
175    private int mSendersX;
176    private int mSendersWidth;
177
178    /** Whether we are on a tablet device or not */
179    private final boolean mTabletDevice;
180    /** When in conversation mode, true if the list is hidden */
181    private final boolean mListCollapsible;
182
183    @VisibleForTesting
184    ConversationItemViewCoordinates mCoordinates;
185
186    private ConversationItemViewCoordinates.Config mConfig;
187
188    private final Context mContext;
189
190    public ConversationItemViewModel mHeader;
191    private boolean mDownEvent;
192    private boolean mSelected = false;
193    private ConversationSelectionSet mSelectedConversationSet;
194    private Folder mDisplayedFolder;
195    private boolean mStarEnabled;
196    private boolean mSwipeEnabled;
197    private int mLastTouchX;
198    private int mLastTouchY;
199    private AnimatedAdapter mAdapter;
200    private float mAnimatedHeightFraction = 1.0f;
201    private final String mAccount;
202    private ControllableActivity mActivity;
203    private final TextView mSendersTextView;
204    private final TextView mSubjectTextView;
205    private final TextView mSnippetTextView;
206    private int mGadgetMode;
207
208    private static int sFoldersStartPadding;
209    private static int sFoldersInnerPadding;
210    private static int sFoldersMaxCount;
211    private static int sFoldersOverflowGradientPadding;
212    private static TextAppearanceSpan sSubjectTextUnreadSpan;
213    private static TextAppearanceSpan sSubjectTextReadSpan;
214    private static TextAppearanceSpan sBadgeTextSpan;
215    private static BackgroundColorSpan sBadgeBackgroundSpan;
216    private static int sScrollSlop;
217    private static CharacterStyle sActivatedTextSpan;
218
219    private final CheckableContactFlipDrawable mSendersImageView;
220
221    /** The resource id of the color to use to override the background. */
222    private int mBackgroundOverrideResId = -1;
223    /** The bitmap to use, or <code>null</code> for the default */
224    private Bitmap mPhotoBitmap = null;
225    private Rect mPhotoRect = null;
226
227    /**
228     * A listener for clicks on the various areas of a conversation item.
229     */
230    public interface ConversationItemAreaClickListener {
231        /** Called when the info icon is clicked. */
232        void onInfoIconClicked();
233
234        /** Called when the star is clicked. */
235        void onStarClicked();
236    }
237
238    /** If set, it will steal all clicks for which the interface has a click method. */
239    private ConversationItemAreaClickListener mConversationItemAreaClickListener = null;
240
241    static {
242        sPaint.setAntiAlias(true);
243        sFoldersPaint.setAntiAlias(true);
244
245        sCheckBackgroundPaint.setColor(Color.GRAY);
246    }
247
248    /**
249     * Handles displaying folders in a conversation header view.
250     */
251    static class ConversationItemFolderDisplayer extends FolderDisplayer {
252
253        private int mFoldersCount;
254
255        public ConversationItemFolderDisplayer(Context context) {
256            super(context);
257        }
258
259        @Override
260        public void loadConversationFolders(Conversation conv, final FolderUri ignoreFolderUri,
261                final int ignoreFolderType) {
262            super.loadConversationFolders(conv, ignoreFolderUri, ignoreFolderType);
263            mFoldersCount = mFoldersSortedSet.size();
264        }
265
266        @Override
267        public void reset() {
268            super.reset();
269            mFoldersCount = 0;
270        }
271
272        public boolean hasVisibleFolders() {
273            return mFoldersCount > 0;
274        }
275
276        /**
277         * Helper function to calculate exactly how much space the displayed folders should take.
278         * @return an array of integers that signifies the length in dp.
279         */
280        private MeasurementWrapper measureFolderDimen(ConversationItemViewCoordinates coordinates) {
281            // This signifies the absolute max for each folder cell, no exceptions.
282            final int maxCellWidth = coordinates.folderCellWidth;
283
284            final int numDisplayedFolders = Math.min(sFoldersMaxCount, mFoldersSortedSet.size());
285            if (numDisplayedFolders == 0) {
286                return new MeasurementWrapper(new int[0], new boolean[0]);
287            }
288
289            // This variable is calculated based on the number of folders we are displaying
290            final int maxAllowedCellSize = Math.min(maxCellWidth, (coordinates.folderLayoutWidth -
291                    (numDisplayedFolders - 1) * sFoldersStartPadding) / numDisplayedFolders);
292            final int[] measurements = new int[numDisplayedFolders];
293            final boolean[] overflow = new boolean[numDisplayedFolders];
294            final MeasurementWrapper result = new MeasurementWrapper(measurements, overflow);
295
296            int count = 0;
297            int missingWidth = 0;
298            int extraWidth = 0;
299            for (Folder f : mFoldersSortedSet) {
300                if (count > numDisplayedFolders - 1) {
301                    break;
302                }
303
304                final String folderString = f.name;
305                final int neededWidth = (int) sFoldersPaint.measureText(folderString) +
306                        2 * sFoldersInnerPadding;
307
308                if (neededWidth > maxAllowedCellSize) {
309                    // What we can take from others is the minimum of the width we need to borrow
310                    // and the width we are allowed to borrow.
311                    final int borrowedWidth = Math.min(neededWidth - maxAllowedCellSize,
312                            maxCellWidth - maxAllowedCellSize);
313                    final int extraWidthLeftover = extraWidth - borrowedWidth;
314                    if (extraWidthLeftover >= 0) {
315                        measurements[count] = Math.min(neededWidth, maxCellWidth);
316                        extraWidth = extraWidthLeftover;
317                    } else {
318                        measurements[count] = maxAllowedCellSize + extraWidth;
319                        extraWidth = 0;
320                    }
321                    missingWidth = -extraWidthLeftover;
322                    overflow[count] = neededWidth > measurements[count];
323                } else {
324                    extraWidth = maxAllowedCellSize - neededWidth;
325                    measurements[count] = neededWidth;
326                    if (missingWidth > 0) {
327                        if (extraWidth >= missingWidth) {
328                            measurements[count - 1] += missingWidth;
329                            extraWidth -= missingWidth;
330                            overflow[count - 1] = false;
331                        } else {
332                            measurements[count - 1] += extraWidth;
333                            extraWidth = 0;
334                        }
335                    }
336                    missingWidth = 0;
337                }
338
339                count++;
340            }
341
342            return result;
343        }
344
345        /**
346         * @return how much total space the folders list requires.
347         */
348        private int measureFolders(ConversationItemViewCoordinates coordinates) {
349            int[] sizes = measureFolderDimen(coordinates).measurements;
350            return sumWidth(sizes);
351        }
352
353        private int sumWidth(int[] arr) {
354            int sum = 0;
355            for (int i = 0; i < arr.length; i++) {
356                sum += arr[i];
357            }
358            return sum + (arr.length - 1) * sFoldersStartPadding;
359        }
360
361        public void drawFolders(
362                Canvas canvas, ConversationItemViewCoordinates coordinates, boolean isRtl) {
363            if (mFoldersCount == 0) {
364                return;
365            }
366
367            final MeasurementWrapper wrapper = measureFolderDimen(coordinates);
368            final int[] measurements = wrapper.measurements;
369            final boolean[] overflow = wrapper.overflow;
370
371            final int right = coordinates.foldersRight;
372            final int y = coordinates.foldersY;
373            final int height = coordinates.foldersHeight;
374            final int textBottomPadding = coordinates.foldersTextBottomPadding;
375
376            sFoldersPaint.setTextSize(coordinates.foldersFontSize);
377            sFoldersPaint.setTypeface(coordinates.foldersTypeface);
378
379            // Initialize space and cell size based on the current mode.
380            final int foldersCount = measurements.length;
381            final int width = sumWidth(measurements);
382            int xLeft = (isRtl) ?  right - coordinates.folderLayoutWidth : right - width;
383
384            int index = 0;
385            for (Folder f : mFoldersSortedSet) {
386                if (index > foldersCount - 1) {
387                    break;
388                }
389
390                final String folderString = f.name;
391                final int fgColor = f.getForegroundColor(mDefaultFgColor);
392                final int bgColor = f.getBackgroundColor(mDefaultBgColor);
393
394                // Draw the box.
395                sFoldersPaint.setColor(bgColor);
396                sFoldersPaint.setStyle(Paint.Style.FILL);
397                final RectF rect =
398                        new RectF(xLeft, y, xLeft + measurements[index], y + height);
399                canvas.drawRoundRect(rect, sFolderRoundedCornerRadius, sFolderRoundedCornerRadius,
400                        sFoldersPaint);
401
402                // Draw the text.
403                sFoldersPaint.setColor(fgColor);
404                sFoldersPaint.setStyle(Paint.Style.FILL);
405                if (overflow[index]) {
406                    final int rightBorder = xLeft + measurements[index];
407                    final int x0 = (isRtl) ? xLeft + sFoldersOverflowGradientPadding :
408                            rightBorder - sFoldersOverflowGradientPadding;
409                    final int x1 = (isRtl) ?  xLeft + sFoldersInnerPadding :
410                            rightBorder - sFoldersInnerPadding;
411                    final Shader shader = new LinearGradient(x0, y, x1, y, fgColor,
412                            Utils.getTransparentColor(fgColor), Shader.TileMode.CLAMP);
413                    sFoldersPaint.setShader(shader);
414                }
415                canvas.drawText(folderString, xLeft + sFoldersInnerPadding,
416                        y + height - textBottomPadding, sFoldersPaint);
417                if (overflow[index]) {
418                    sFoldersPaint.setShader(null);
419                }
420
421                xLeft += measurements[index++] + sFoldersStartPadding;
422            }
423        }
424
425        private static class MeasurementWrapper {
426            final int[] measurements;
427            final boolean[] overflow;
428
429            public MeasurementWrapper(int[] m, boolean[] o) {
430                measurements = m;
431                overflow = o;
432            }
433        }
434    }
435
436    public ConversationItemView(Context context, String account) {
437        super(context);
438        Utils.traceBeginSection("CIVC constructor");
439        setClickable(true);
440        setLongClickable(true);
441        mContext = context.getApplicationContext();
442        final Resources res = mContext.getResources();
443        mTabletDevice = Utils.useTabletUI(res);
444        mListCollapsible = res.getBoolean(R.bool.list_collapsible);
445        mAccount = account;
446
447        getItemViewResources(mContext);
448
449        final int layoutDir = TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault());
450
451        mSendersTextView = new TextView(mContext);
452        mSendersTextView.setIncludeFontPadding(false);
453
454        mSubjectTextView = new TextView(mContext);
455        mSubjectTextView.setEllipsize(TextUtils.TruncateAt.END);
456        mSubjectTextView.setSingleLine(); // allow partial words to be elided
457        mSubjectTextView.setIncludeFontPadding(false);
458        ViewCompat.setLayoutDirection(mSubjectTextView, layoutDir);
459        ViewUtils.setTextAlignment(mSubjectTextView, View.TEXT_ALIGNMENT_VIEW_START);
460
461        mSnippetTextView = new TextView(mContext);
462        mSnippetTextView.setEllipsize(TextUtils.TruncateAt.END);
463        mSnippetTextView.setSingleLine(); // allow partial words to be elided
464        mSnippetTextView.setIncludeFontPadding(false);
465        mSnippetTextView.setTypeface(SANS_SERIF_LIGHT);
466        mSnippetTextView.setTextColor(getResources().getColor(R.color.snippet_text_color));
467        ViewCompat.setLayoutDirection(mSnippetTextView, layoutDir);
468        ViewUtils.setTextAlignment(mSnippetTextView, View.TEXT_ALIGNMENT_VIEW_START);
469
470        mSendersImageView = new CheckableContactFlipDrawable(res, sCabAnimationDuration);
471        mSendersImageView.setCallback(this);
472
473        Utils.traceEndSection();
474    }
475
476    private static synchronized void getItemViewResources(Context context) {
477        if (sConfigurationChangedReceiver == null) {
478            sConfigurationChangedReceiver = new BroadcastReceiver() {
479                @Override
480                public void onReceive(Context context, Intent intent) {
481                    STAR_OFF = null;
482                    getItemViewResources(context);
483                }
484            };
485            context.registerReceiver(sConfigurationChangedReceiver, new IntentFilter(
486                    Intent.ACTION_CONFIGURATION_CHANGED));
487        }
488        if (STAR_OFF == null) {
489            final Resources res = context.getResources();
490            // Initialize static bitmaps.
491            STAR_OFF = BitmapFactory.decodeResource(res, R.drawable.ic_star_outline_20dp);
492            STAR_ON = BitmapFactory.decodeResource(res, R.drawable.ic_star_20dp);
493            ATTACHMENT = BitmapFactory.decodeResource(res, R.drawable.ic_attach_file_20dp);
494            ONLY_TO_ME = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_double);
495            TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_single);
496            IMPORTANT_ONLY_TO_ME = BitmapFactory.decodeResource(res,
497                    R.drawable.ic_email_caret_double_important_unread);
498            IMPORTANT_TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res,
499                    R.drawable.ic_email_caret_single_important_unread);
500            IMPORTANT = BitmapFactory.decodeResource(res,
501                    R.drawable.ic_email_caret_none_important_unread);
502            STATE_REPLIED =
503                    BitmapFactory.decodeResource(res, R.drawable.ic_badge_reply_holo_light);
504            STATE_FORWARDED =
505                    BitmapFactory.decodeResource(res, R.drawable.ic_badge_forward_holo_light);
506            STATE_REPLIED_AND_FORWARDED =
507                    BitmapFactory.decodeResource(res, R.drawable.ic_badge_reply_forward_holo_light);
508            STATE_CALENDAR_INVITE =
509                    BitmapFactory.decodeResource(res, R.drawable.ic_badge_invite_holo_light);
510            VISIBLE_CONVERSATION_HIGHLIGHT = res.getDrawable(
511                    R.drawable.visible_conversation_highlight);
512            RIGHT_EDGE_TABLET = res.getDrawable(R.drawable.list_edge_tablet);
513
514            // Initialize colors.
515            sActivatedTextSpan = CharacterStyle.wrap(new ForegroundColorSpan(
516                    res.getColor(R.color.senders_text_color)));
517            sSendersTextColor = res.getColor(R.color.senders_text_color);
518            sSubjectTextUnreadSpan = new TextAppearanceSpan(context,
519                    R.style.SubjectAppearanceUnreadStyle);
520            sSubjectTextReadSpan = new TextAppearanceSpan(
521                    context, R.style.SubjectAppearanceReadStyle);
522
523            sBadgeTextSpan = new TextAppearanceSpan(context, R.style.BadgeTextStyle);
524            sBadgeBackgroundSpan = new BackgroundColorSpan(
525                    res.getColor(R.color.badge_background_color));
526            sDateTextColorRead = res.getColor(R.color.date_text_color_read);
527            sDateTextColorUnread = res.getColor(R.color.date_text_color_unread);
528            sStarTouchSlop = res.getDimensionPixelSize(R.dimen.star_touch_slop);
529            sSenderImageTouchSlop = res.getDimensionPixelSize(R.dimen.sender_image_touch_slop);
530            sShrinkAnimationDuration = res.getInteger(R.integer.shrink_animation_duration);
531            sSlideAnimationDuration = res.getInteger(R.integer.slide_animation_duration);
532            // Initialize static color.
533            sSendersSplitToken = res.getString(R.string.senders_split_token);
534            sElidedPaddingToken = res.getString(R.string.elided_padding_token);
535            sScrollSlop = res.getInteger(R.integer.swipeScrollSlop);
536            sFoldersStartPadding = res.getDimensionPixelOffset(R.dimen.folders_start_padding);
537            sFoldersInnerPadding = res.getDimensionPixelOffset(R.dimen.folder_cell_content_padding);
538            sFoldersMaxCount = res.getInteger(R.integer.conversation_list_max_folder_count);
539            sFoldersOverflowGradientPadding =
540                    res.getDimensionPixelOffset(R.dimen.folders_gradient_padding);
541            sCabAnimationDuration = res.getInteger(R.integer.conv_item_view_cab_anim_duration);
542            sBadgePaddingExtraWidth = res.getDimensionPixelSize(R.dimen.badge_padding_extra_width);
543            sBadgeRoundedCornerRadius =
544                    res.getDimensionPixelSize(R.dimen.badge_rounded_corner_radius);
545            sFolderRoundedCornerRadius =
546                    res.getDimensionPixelOffset(R.dimen.folder_rounded_corner_radius);
547            sDividerColor = res.getColor(R.color.conversation_list_divider_color);
548            sDividerInset = res.getDimensionPixelSize(R.dimen.conv_list_divider_inset);
549            sDividerHeight = res.getDimensionPixelSize(R.dimen.divider_height);
550        }
551    }
552
553    public void bind(final Conversation conversation, final ControllableActivity activity,
554            final ConversationSelectionSet set, final Folder folder,
555            final int checkboxOrSenderImage,
556            final boolean swipeEnabled, final boolean importanceMarkersEnabled,
557            final boolean showChevronsEnabled, final AnimatedAdapter adapter) {
558        Utils.traceBeginSection("CIVC.bind");
559        bind(ConversationItemViewModel.forConversation(mAccount, conversation), activity,
560                null /* conversationItemAreaClickListener */,
561                set, folder, checkboxOrSenderImage, swipeEnabled, importanceMarkersEnabled,
562                showChevronsEnabled, adapter, -1 /* backgroundOverrideResId */,
563                null /* photoBitmap */, false /* useFullMargins */);
564        Utils.traceEndSection();
565    }
566
567    public void bindAd(final ConversationItemViewModel conversationItemViewModel,
568            final ControllableActivity activity,
569            final ConversationItemAreaClickListener conversationItemAreaClickListener,
570            final Folder folder, final int checkboxOrSenderImage, final AnimatedAdapter adapter,
571            final int backgroundOverrideResId, final Bitmap photoBitmap) {
572        Utils.traceBeginSection("CIVC.bindAd");
573        bind(conversationItemViewModel, activity, conversationItemAreaClickListener, null /* set */,
574                folder, checkboxOrSenderImage, true /* swipeEnabled */,
575                false /* importanceMarkersEnabled */, false /* showChevronsEnabled */,
576                adapter, backgroundOverrideResId, photoBitmap, true /* useFullMargins */);
577        Utils.traceEndSection();
578    }
579
580    private void bind(final ConversationItemViewModel header, final ControllableActivity activity,
581            final ConversationItemAreaClickListener conversationItemAreaClickListener,
582            final ConversationSelectionSet set, final Folder folder,
583            final int checkboxOrSenderImage,
584            boolean swipeEnabled, final boolean importanceMarkersEnabled,
585            final boolean showChevronsEnabled, final AnimatedAdapter adapter,
586            final int backgroundOverrideResId, final Bitmap photoBitmap,
587            final boolean useFullMargins) {
588        mBackgroundOverrideResId = backgroundOverrideResId;
589        mPhotoBitmap = photoBitmap;
590        mConversationItemAreaClickListener = conversationItemAreaClickListener;
591
592        if (mHeader != null) {
593            Utils.traceBeginSection("unbind");
594            final boolean newlyBound = header.conversation.id != mHeader.conversation.id;
595            // If this was previously bound to a different conversation, remove any contact photo
596            // manager requests.
597            if (newlyBound || (mHeader.displayableNames != null && !mHeader
598                    .displayableNames.equals(header.displayableNames))) {
599                mSendersImageView.getContactDrawable().unbind();
600            }
601
602            if (newlyBound) {
603                // Stop the photo flip animation
604                final boolean showSenders = !isSelected();
605                mSendersImageView.reset(showSenders);
606            }
607            Utils.traceEndSection();
608        }
609        mCoordinates = null;
610        mHeader = header;
611        mActivity = activity;
612        mSelectedConversationSet = set;
613        if (mSelectedConversationSet != null) {
614            mSelectedConversationSet.addObserver(this);
615        }
616        mDisplayedFolder = folder;
617        mStarEnabled = folder != null && !folder.isTrash();
618        mSwipeEnabled = swipeEnabled;
619        mAdapter = adapter;
620
621        Utils.traceBeginSection("drawables");
622        mSendersImageView.getContactDrawable().setBitmapCache(mAdapter.getSendersImagesCache());
623        mSendersImageView.getContactDrawable().setContactResolver(mAdapter.getContactResolver());
624        Utils.traceEndSection();
625
626        if (checkboxOrSenderImage == ConversationListIcon.SENDER_IMAGE) {
627            mGadgetMode = ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO;
628        } else {
629            mGadgetMode = ConversationItemViewCoordinates.GADGET_NONE;
630        }
631
632        Utils.traceBeginSection("folder displayer");
633        // Initialize folder displayer.
634        if (mHeader.folderDisplayer == null) {
635            mHeader.folderDisplayer = new ConversationItemFolderDisplayer(mContext);
636        } else {
637            mHeader.folderDisplayer.reset();
638        }
639        Utils.traceEndSection();
640
641        final int ignoreFolderType;
642        if (mDisplayedFolder.isInbox()) {
643            ignoreFolderType = FolderType.INBOX;
644        } else {
645            ignoreFolderType = -1;
646        }
647
648        Utils.traceBeginSection("load folders");
649        mHeader.folderDisplayer.loadConversationFolders(mHeader.conversation,
650                mDisplayedFolder.folderUri, ignoreFolderType);
651        Utils.traceEndSection();
652
653        if (mHeader.showDateText) {
654            Utils.traceBeginSection("relative time");
655            mHeader.dateText = DateUtils.getRelativeTimeSpanString(mContext,
656                    mHeader.conversation.dateMs);
657            Utils.traceEndSection();
658        } else {
659            mHeader.dateText = "";
660        }
661
662        Utils.traceBeginSection("config setup");
663        mConfig = new ConversationItemViewCoordinates.Config()
664            .withGadget(mGadgetMode)
665            .setUseFullMargins(useFullMargins);
666        if (header.folderDisplayer.hasVisibleFolders()) {
667            mConfig.showFolders();
668        }
669        if (header.hasBeenForwarded || header.hasBeenRepliedTo || header.isInvite) {
670            mConfig.showReplyState();
671        }
672        if (mHeader.conversation.color != 0) {
673            mConfig.showColorBlock();
674        }
675
676        // Importance markers and chevrons (personal level indicators).
677        mHeader.personalLevelBitmap = null;
678        final int personalLevel = mHeader.conversation.personalLevel;
679        final boolean isImportant =
680                mHeader.conversation.priority == UIProvider.ConversationPriority.IMPORTANT;
681        final boolean useImportantMarkers = isImportant && importanceMarkersEnabled;
682        if (showChevronsEnabled &&
683                personalLevel == UIProvider.ConversationPersonalLevel.ONLY_TO_ME) {
684            mHeader.personalLevelBitmap = useImportantMarkers ? IMPORTANT_ONLY_TO_ME
685                    : ONLY_TO_ME;
686        } else if (showChevronsEnabled &&
687                personalLevel == UIProvider.ConversationPersonalLevel.TO_ME_AND_OTHERS) {
688            mHeader.personalLevelBitmap = useImportantMarkers ? IMPORTANT_TO_ME_AND_OTHERS
689                    : TO_ME_AND_OTHERS;
690        } else if (useImportantMarkers) {
691            mHeader.personalLevelBitmap = IMPORTANT;
692        }
693        if (mHeader.personalLevelBitmap != null) {
694            mConfig.showPersonalIndicator();
695        }
696        Utils.traceEndSection();
697
698        Utils.traceBeginSection("content description");
699        setContentDescription();
700        Utils.traceEndSection();
701        requestLayout();
702    }
703
704    @Override
705    protected void onDetachedFromWindow() {
706        super.onDetachedFromWindow();
707
708        if (mSelectedConversationSet != null) {
709            mSelectedConversationSet.removeObserver(this);
710        }
711    }
712
713    @Override
714    public void invalidateDrawable(final Drawable who) {
715        boolean handled = false;
716        if (mCoordinates != null) {
717            if (mSendersImageView.equals(who)) {
718                final Rect r = new Rect(who.getBounds());
719                r.offset(mCoordinates.contactImagesX, mCoordinates.contactImagesY);
720                ConversationItemView.this.invalidate(r.left, r.top, r.right, r.bottom);
721                handled = true;
722            }
723        }
724        if (!handled) {
725            super.invalidateDrawable(who);
726        }
727    }
728
729    /**
730     * Get the Conversation object associated with this view.
731     */
732    public Conversation getConversation() {
733        return mHeader.conversation;
734    }
735
736    private static void startTimer(String tag) {
737        if (sTimer != null) {
738            sTimer.start(tag);
739        }
740    }
741
742    private static void pauseTimer(String tag) {
743        if (sTimer != null) {
744            sTimer.pause(tag);
745        }
746    }
747
748    @Override
749    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
750        Utils.traceBeginSection("CIVC.measure");
751        final int wSize = MeasureSpec.getSize(widthMeasureSpec);
752
753        final int currentMode = mActivity.getViewMode().getMode();
754        if (wSize != mViewWidth || mPreviousMode != currentMode) {
755            mViewWidth = wSize;
756            mPreviousMode = currentMode;
757        }
758        mHeader.viewWidth = mViewWidth;
759
760        mConfig.updateWidth(wSize).setViewMode(currentMode)
761                .setLayoutDirection(ViewCompat.getLayoutDirection(this));
762
763        Resources res = getResources();
764        mHeader.standardScaledDimen = res.getDimensionPixelOffset(R.dimen.standard_scaled_dimen);
765
766        mCoordinates = ConversationItemViewCoordinates.forConfig(mContext, mConfig,
767                mAdapter.getCoordinatesCache());
768
769        if (mPhotoBitmap != null) {
770            mPhotoRect = new Rect(0, 0, mCoordinates.contactImagesWidth,
771                    mCoordinates.contactImagesHeight);
772        }
773
774        final int h = (mAnimatedHeightFraction != 1.0f) ?
775                Math.round(mAnimatedHeightFraction * mCoordinates.height) : mCoordinates.height;
776        setMeasuredDimension(mConfig.getWidth(), h);
777        Utils.traceEndSection();
778    }
779
780    @Override
781    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
782        startTimer(PERF_TAG_LAYOUT);
783        Utils.traceBeginSection("CIVC.layout");
784
785        super.onLayout(changed, left, top, right, bottom);
786
787        Utils.traceBeginSection("text and bitmaps");
788        calculateTextsAndBitmaps();
789        Utils.traceEndSection();
790
791        Utils.traceBeginSection("coordinates");
792        calculateCoordinates();
793        Utils.traceEndSection();
794
795        // Subject.
796        Utils.traceBeginSection("subject");
797        createSubject(mHeader.unread);
798
799        createSnippet();
800
801        if (!mHeader.isLayoutValid()) {
802            setContentDescription();
803        }
804        mHeader.validate();
805        Utils.traceEndSection();
806
807        pauseTimer(PERF_TAG_LAYOUT);
808        if (sTimer != null && ++sLayoutCount >= PERF_LAYOUT_ITERATIONS) {
809            sTimer.dumpResults();
810            sTimer = new Timer();
811            sLayoutCount = 0;
812        }
813        Utils.traceEndSection();
814    }
815
816    private void setContentDescription() {
817        if (mActivity.isAccessibilityEnabled()) {
818            mHeader.resetContentDescription();
819            setContentDescription(
820                    mHeader.getContentDescription(mContext, mDisplayedFolder.shouldShowRecipients()));
821        }
822    }
823
824    @Override
825    public void setBackgroundResource(int resourceId) {
826        Utils.traceBeginSection("set background resource");
827        Drawable drawable = mBackgrounds.get(resourceId);
828        if (drawable == null) {
829            drawable = getResources().getDrawable(resourceId);
830            final int insetPadding = mHeader.insetPadding;
831            if (insetPadding > 0) {
832                drawable = new InsetDrawable(drawable, insetPadding);
833            }
834            mBackgrounds.put(resourceId, drawable);
835        }
836        if (getBackground() != drawable) {
837            super.setBackgroundDrawable(drawable);
838        }
839        Utils.traceEndSection();
840    }
841
842    private void calculateTextsAndBitmaps() {
843        startTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
844
845        if (mSelectedConversationSet != null) {
846            mSelected = mSelectedConversationSet.contains(mHeader.conversation);
847        }
848        setSelected(mSelected);
849        mHeader.gadgetMode = mGadgetMode;
850
851        updateBackground();
852
853        mHeader.sendersDisplayText = new SpannableStringBuilder();
854
855        mHeader.hasDraftMessage = mHeader.conversation.numDrafts() > 0;
856
857        // Parse senders fragments.
858        if (mHeader.preserveSendersText) {
859            // This is a special view that doesn't need special sender formatting
860            mHeader.sendersDisplayText = new SpannableStringBuilder(mHeader.sendersText);
861            loadImages();
862        } else if (mHeader.conversation.conversationInfo != null) {
863            Context context = getContext();
864            mHeader.messageInfoString = SendersView
865                    .createMessageInfo(context, mHeader.conversation, true);
866            int maxChars = ConversationItemViewCoordinates.getSendersLength(context,
867                    mCoordinates.getMode(), mHeader.conversation.hasAttachments);
868            mHeader.displayableEmails = new ArrayList<String>();
869            mHeader.displayableNames = new ArrayList<String>();
870            mHeader.styledNames = new ArrayList<SpannableString>();
871
872            SendersView.format(context, mHeader.conversation.conversationInfo,
873                    mHeader.messageInfoString.toString(), maxChars, mHeader.styledNames,
874                    mHeader.displayableNames, mHeader.displayableEmails, mAccount,
875                    mDisplayedFolder.shouldShowRecipients(), true);
876
877            if (mHeader.displayableEmails.isEmpty() && mHeader.hasDraftMessage) {
878                mHeader.displayableEmails.add(mAccount);
879                mHeader.displayableNames.add(mAccount);
880            }
881
882            // If we have displayable senders, load their thumbnails
883            loadImages();
884        } else {
885            LogUtils.wtf(LOG_TAG, "Null conversationInfo");
886        }
887
888        if (mHeader.isLayoutValid()) {
889            pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
890            return;
891        }
892        startTimer(PERF_TAG_CALCULATE_FOLDERS);
893
894
895        pauseTimer(PERF_TAG_CALCULATE_FOLDERS);
896
897        // Paper clip icon.
898        mHeader.paperclip = null;
899        if (mHeader.conversation.hasAttachments) {
900            mHeader.paperclip = ATTACHMENT;
901        }
902
903        startTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT);
904
905        pauseTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT);
906        pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
907    }
908
909    // FIXME(ath): maybe move this to bind(). the only dependency on layout is on tile W/H, which
910    // is immutable.
911    private void loadImages() {
912        if (mGadgetMode != ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO
913                || mHeader.displayableEmails == null
914                || mHeader.displayableEmails.isEmpty()) {
915            return;
916        }
917        if (mCoordinates.contactImagesWidth <= 0 || mCoordinates.contactImagesHeight <= 0) {
918            LogUtils.w(LOG_TAG,
919                    "Contact image width(%d) or height(%d) is 0 for mode: (%d).",
920                    mCoordinates.contactImagesWidth, mCoordinates.contactImagesHeight,
921                    mCoordinates.getMode());
922            return;
923        }
924
925        mSendersImageView
926                .setBounds(0, 0, mCoordinates.contactImagesWidth, mCoordinates.contactImagesHeight);
927
928        Utils.traceBeginSection("load sender image");
929        final ContactDrawable drawable = mSendersImageView.getContactDrawable();
930        drawable.setDecodeDimensions(mCoordinates.contactImagesWidth,
931                mCoordinates.contactImagesHeight);
932        drawable.bind(mHeader.displayableNames.get(0), mHeader.displayableEmails.get(0));
933        Utils.traceEndSection();
934    }
935
936    private static int makeExactSpecForSize(int size) {
937        return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
938    }
939
940    private static void layoutViewExactly(View v, int w, int h) {
941        v.measure(makeExactSpecForSize(w), makeExactSpecForSize(h));
942        v.layout(0, 0, w, h);
943    }
944
945    private void layoutParticipantText(SpannableStringBuilder participantText) {
946        if (participantText != null) {
947            if (isActivated() && showActivatedText()) {
948                participantText.setSpan(sActivatedTextSpan, 0,
949                        mHeader.styledMessageInfoStringOffset, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
950            } else {
951                participantText.removeSpan(sActivatedTextSpan);
952            }
953
954            final int w = mSendersWidth;
955            final int h = mCoordinates.sendersHeight;
956            mSendersTextView.setLayoutParams(new ViewGroup.LayoutParams(w, h));
957            mSendersTextView.setMaxLines(mCoordinates.sendersLineCount);
958            mSendersTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCoordinates.sendersFontSize);
959            layoutViewExactly(mSendersTextView, w, h);
960
961            mSendersTextView.setText(participantText);
962        }
963    }
964
965    private void createSubject(final boolean isUnread) {
966        final String badgeText = mHeader.badgeText == null ? "" : mHeader.badgeText;
967        String subject = filterTag(getContext(), mHeader.conversation.subject);
968        subject = Conversation.getSubjectForDisplay(mContext, badgeText, subject);
969        final Spannable displayedStringBuilder = new SpannableString(subject);
970
971        // since spans affect text metrics, add spans to the string before measure/layout or fancy
972        // ellipsizing
973
974        final int badgeTextLength = formatBadgeText(displayedStringBuilder, badgeText);
975
976        if (!TextUtils.isEmpty(subject)) {
977            displayedStringBuilder.setSpan(TextAppearanceSpan.wrap(
978                    isUnread ? sSubjectTextUnreadSpan : sSubjectTextReadSpan),
979                    badgeTextLength, subject.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
980        }
981        if (isActivated() && showActivatedText()) {
982            displayedStringBuilder.setSpan(sActivatedTextSpan, badgeTextLength,
983                    displayedStringBuilder.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
984        }
985
986        final int subjectWidth = mCoordinates.subjectWidth;
987        final int subjectHeight = mCoordinates.subjectHeight;
988        mSubjectTextView.setLayoutParams(new ViewGroup.LayoutParams(subjectWidth, subjectHeight));
989        mSubjectTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCoordinates.subjectFontSize);
990        layoutViewExactly(mSubjectTextView, subjectWidth, subjectHeight);
991
992        mSubjectTextView.setText(displayedStringBuilder);
993    }
994
995    private void createSnippet() {
996        final String snippet = mHeader.conversation.getSnippet();
997        final Spannable displayedStringBuilder = new SpannableString(snippet);
998
999        // measure the width of the folders which overlap the snippet view
1000        final int folderWidth = mHeader.folderDisplayer.measureFolders(mCoordinates);
1001
1002        // size the snippet view by subtracting the folder width from the maximum snippet width
1003        final int snippetWidth = mCoordinates.maxSnippetWidth - folderWidth;
1004        final int snippetHeight = mCoordinates.snippetHeight;
1005        mSnippetTextView.setLayoutParams(new ViewGroup.LayoutParams(snippetWidth, snippetHeight));
1006        mSnippetTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCoordinates.snippetFontSize);
1007        layoutViewExactly(mSnippetTextView, snippetWidth, snippetHeight);
1008
1009        mSnippetTextView.setText(displayedStringBuilder);
1010    }
1011
1012    private int formatBadgeText(Spannable displayedStringBuilder, String badgeText) {
1013        final int badgeTextLength = (badgeText != null) ? badgeText.length() : 0;
1014        if (!TextUtils.isEmpty(badgeText)) {
1015            displayedStringBuilder.setSpan(TextAppearanceSpan.wrap(sBadgeTextSpan),
1016                    0, badgeTextLength, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1017            displayedStringBuilder.setSpan(TextAppearanceSpan.wrap(sBadgeBackgroundSpan),
1018                    0, badgeTextLength, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1019            displayedStringBuilder.setSpan(new BadgeSpan(displayedStringBuilder, this),
1020                    0, badgeTextLength, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1021        }
1022
1023        return badgeTextLength;
1024    }
1025
1026    // START BadgeSpan.BadgeSpanDimensions override
1027
1028    @Override
1029    public int getHorizontalPadding() {
1030        return sBadgePaddingExtraWidth;
1031    }
1032
1033    @Override
1034    public float getRoundedCornerRadius() {
1035        return sBadgeRoundedCornerRadius;
1036    }
1037
1038    // END BadgeSpan.BadgeSpanDimensions override
1039
1040    private boolean showActivatedText() {
1041        // For activated elements in tablet in conversation mode, we show an activated color, since
1042        // the background is dark blue for activated versus gray for non-activated.
1043        return mTabletDevice && !mListCollapsible;
1044    }
1045
1046    private void calculateCoordinates() {
1047        startTimer(PERF_TAG_CALCULATE_COORDINATES);
1048
1049        sPaint.setTextSize(mCoordinates.dateFontSize);
1050        sPaint.setTypeface(Typeface.DEFAULT);
1051
1052        final boolean isRtl = ViewUtils.isViewRtl(this);
1053
1054        mDateWidth = (int) sPaint.measureText(
1055                mHeader.dateText != null ? mHeader.dateText.toString() : "");
1056        if (mHeader.infoIcon != null) {
1057            mInfoIconX = (isRtl) ? mCoordinates.infoIconX :
1058                    mCoordinates.infoIconXRight - mHeader.infoIcon.getWidth();
1059
1060            // If we have an info icon, we start drawing the date text:
1061            // At the end of the date TextView minus the width of the date text
1062            // In RTL mode, we just use dateX
1063            mDateX = (isRtl) ? mCoordinates.dateX : mCoordinates.dateXRight - mDateWidth;
1064        } else {
1065            // If there is no info icon, we start drawing the date text:
1066            // At the end of the info icon ImageView minus the width of the date text
1067            // We use the info icon ImageView for positioning, since we want the date text to be
1068            // at the right, since there is no info icon
1069            // In RTL, we just use infoIconX
1070            mDateX = (isRtl) ? mCoordinates.infoIconX : mCoordinates.infoIconXRight - mDateWidth;
1071        }
1072
1073        // The paperclip is drawn starting at the start of the date text minus
1074        // the width of the paperclip and the date padding.
1075        // In RTL mode, it is at the end of the date (mDateX + mDateWidth) plus the
1076        // start date padding.
1077        mPaperclipX = (isRtl) ? mDateX + mDateWidth + mCoordinates.datePaddingStart :
1078                mDateX - ATTACHMENT.getWidth() - mCoordinates.datePaddingStart;
1079
1080        // In normal mode, the senders x and width is based
1081        // on where the date/attachment icon start.
1082        final int dateAttachmentStart;
1083        // Have this end near the paperclip or date, not the folders.
1084        if (mHeader.paperclip != null) {
1085            // If there is a paperclip, the date/attachment start is at the start
1086            // of the paperclip minus the paperclip padding.
1087            // In RTL, it is at the end of the paperclip plus the paperclip padding.
1088            dateAttachmentStart = (isRtl) ?
1089                    mPaperclipX + ATTACHMENT.getWidth() + mCoordinates.paperclipPaddingStart
1090                    : mPaperclipX - mCoordinates.paperclipPaddingStart;
1091        } else {
1092            // If no paperclip, just use the start of the date minus the date padding start.
1093            // In RTL mode, this is just the paperclipX.
1094            dateAttachmentStart = (isRtl) ?
1095                    mPaperclipX : mDateX - mCoordinates.datePaddingStart;
1096        }
1097        // Senders width is the dateAttachmentStart - sendersX.
1098        // In RTL, it is sendersWidth + sendersX - dateAttachmentStart.
1099        mSendersWidth = (isRtl) ?
1100                mCoordinates.sendersWidth + mCoordinates.sendersX - dateAttachmentStart
1101                : dateAttachmentStart - mCoordinates.sendersX;
1102        mSendersX = (isRtl) ? dateAttachmentStart : mCoordinates.sendersX;
1103
1104        // Second pass to layout each fragment.
1105        sPaint.setTextSize(mCoordinates.sendersFontSize);
1106        sPaint.setTypeface(Typeface.DEFAULT);
1107
1108        if (mHeader.styledNames != null) {
1109            final SpannableStringBuilder participantText = elideParticipants(mHeader.styledNames);
1110            layoutParticipantText(participantText);
1111        } else {
1112            // First pass to calculate width of each fragment.
1113            if (mSendersWidth < 0) {
1114                mSendersWidth = 0;
1115            }
1116
1117            mHeader.sendersDisplayLayout = new StaticLayout(mHeader.sendersDisplayText, sPaint,
1118                    mSendersWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
1119        }
1120
1121        if (mSendersWidth < 0) {
1122            mSendersWidth = 0;
1123        }
1124
1125        pauseTimer(PERF_TAG_CALCULATE_COORDINATES);
1126    }
1127
1128    // The rules for displaying elided participants are as follows:
1129    // 1) If there is message info (either a COUNT or DRAFT info to display), it MUST be shown
1130    // 2) If senders do not fit, ellipsize the last one that does fit, and stop
1131    // appending new senders
1132    SpannableStringBuilder elideParticipants(List<SpannableString> parts) {
1133        final SpannableStringBuilder builder = new SpannableStringBuilder();
1134        float totalWidth = 0;
1135        boolean ellipsize = false;
1136        float width;
1137        boolean skipToHeader = false;
1138
1139        // start with "To: " if we're showing recipients
1140        if (mDisplayedFolder.shouldShowRecipients() && !parts.isEmpty()) {
1141            final SpannableString toHeader = SendersView.getFormattedToHeader();
1142            CharacterStyle[] spans = toHeader.getSpans(0, toHeader.length(),
1143                    CharacterStyle.class);
1144            // There is only 1 character style span; make sure we apply all the
1145            // styles to the paint object before measuring.
1146            if (spans.length > 0) {
1147                spans[0].updateDrawState(sPaint);
1148            }
1149            totalWidth += sPaint.measureText(toHeader.toString());
1150            builder.append(toHeader);
1151            skipToHeader = true;
1152        }
1153
1154        final SpannableStringBuilder messageInfoString = mHeader.messageInfoString;
1155        if (messageInfoString.length() > 0) {
1156            CharacterStyle[] spans = messageInfoString.getSpans(0, messageInfoString.length(),
1157                    CharacterStyle.class);
1158            // There is only 1 character style span; make sure we apply all the
1159            // styles to the paint object before measuring.
1160            if (spans.length > 0) {
1161                spans[0].updateDrawState(sPaint);
1162            }
1163            // Paint the message info string to see if we lose space.
1164            float messageInfoWidth = sPaint.measureText(messageInfoString.toString());
1165            totalWidth += messageInfoWidth;
1166        }
1167       SpannableString prevSender = null;
1168       SpannableString ellipsizedText;
1169        for (SpannableString sender : parts) {
1170            // There may be null sender strings if there were dupes we had to remove.
1171            if (sender == null) {
1172                continue;
1173            }
1174            // No more width available, we'll only show fixed fragments.
1175            if (ellipsize) {
1176                break;
1177            }
1178            CharacterStyle[] spans = sender.getSpans(0, sender.length(), CharacterStyle.class);
1179            // There is only 1 character style span.
1180            if (spans.length > 0) {
1181                spans[0].updateDrawState(sPaint);
1182            }
1183            // If there are already senders present in this string, we need to
1184            // make sure we prepend the dividing token
1185            if (SendersView.sElidedString.equals(sender.toString())) {
1186                prevSender = sender;
1187                sender = copyStyles(spans, sElidedPaddingToken + sender + sElidedPaddingToken);
1188            } else if (!skipToHeader && builder.length() > 0
1189                    && (prevSender == null || !SendersView.sElidedString.equals(prevSender
1190                            .toString()))) {
1191                prevSender = sender;
1192                sender = copyStyles(spans, sSendersSplitToken + sender);
1193            } else {
1194                prevSender = sender;
1195                skipToHeader = false;
1196            }
1197            if (spans.length > 0) {
1198                spans[0].updateDrawState(sPaint);
1199            }
1200            // Measure the width of the current sender and make sure we have space
1201            width = (int) sPaint.measureText(sender.toString());
1202            if (width + totalWidth > mSendersWidth) {
1203                // The text is too long, new line won't help. We have to
1204                // ellipsize text.
1205                ellipsize = true;
1206                width = mSendersWidth - totalWidth; // ellipsis width?
1207                ellipsizedText = copyStyles(spans,
1208                        TextUtils.ellipsize(sender, sPaint, width, TruncateAt.END));
1209                width = (int) sPaint.measureText(ellipsizedText.toString());
1210            } else {
1211                ellipsizedText = null;
1212            }
1213            totalWidth += width;
1214
1215            final CharSequence fragmentDisplayText;
1216            if (ellipsizedText != null) {
1217                fragmentDisplayText = ellipsizedText;
1218            } else {
1219                fragmentDisplayText = sender;
1220            }
1221            builder.append(fragmentDisplayText);
1222        }
1223        mHeader.styledMessageInfoStringOffset = builder.length();
1224        builder.append(messageInfoString);
1225        return builder;
1226    }
1227
1228    private static SpannableString copyStyles(CharacterStyle[] spans, CharSequence newText) {
1229        SpannableString s = new SpannableString(newText);
1230        if (spans != null && spans.length > 0) {
1231            s.setSpan(spans[0], 0, s.length(), 0);
1232        }
1233        return s;
1234    }
1235
1236    /**
1237     * If the subject contains the tag of a mailing-list (text surrounded with
1238     * []), return the subject with that tag ellipsized, e.g.
1239     * "[android-gmail-team] Hello" -> "[andr...] Hello"
1240     */
1241    public static String filterTag(Context context, String subject) {
1242        String result = subject;
1243        String formatString = context.getResources().getString(R.string.filtered_tag);
1244        if (!TextUtils.isEmpty(subject) && subject.charAt(0) == '[') {
1245            int end = subject.indexOf(']');
1246            if (end > 0) {
1247                String tag = subject.substring(1, end);
1248                result = String.format(formatString, Utils.ellipsize(tag, 7),
1249                        subject.substring(end + 1));
1250            }
1251        }
1252        return result;
1253    }
1254
1255    @Override
1256    protected void onDraw(Canvas canvas) {
1257        Utils.traceBeginSection("CIVC.draw");
1258
1259        // Contact photo
1260        if (mGadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO) {
1261            canvas.save();
1262            Utils.traceBeginSection("draw senders image");
1263            drawSendersImage(canvas);
1264            Utils.traceEndSection();
1265            canvas.restore();
1266        }
1267
1268        // Senders.
1269        boolean isUnread = mHeader.unread;
1270        // Old style senders; apply text colors/ sizes/ styling.
1271        canvas.save();
1272        if (mHeader.sendersDisplayLayout != null) {
1273            sPaint.setTextSize(mCoordinates.sendersFontSize);
1274            sPaint.setTypeface(SendersView.getTypeface(isUnread));
1275            sPaint.setColor(sSendersTextColor);
1276            canvas.translate(mSendersX, mCoordinates.sendersY
1277                    + mHeader.sendersDisplayLayout.getTopPadding());
1278            mHeader.sendersDisplayLayout.draw(canvas);
1279        } else {
1280            drawSenders(canvas);
1281        }
1282        canvas.restore();
1283
1284
1285        // Subject.
1286        sPaint.setTypeface(Typeface.DEFAULT);
1287        canvas.save();
1288        drawSubject(canvas);
1289        canvas.restore();
1290
1291        canvas.save();
1292        drawSnippet(canvas);
1293        canvas.restore();
1294
1295        // Folders.
1296        if (mConfig.areFoldersVisible()) {
1297            mHeader.folderDisplayer.drawFolders(canvas, mCoordinates, ViewUtils.isViewRtl(this));
1298        }
1299
1300        // If this folder has a color (combined view/Email), show it here
1301        if (mConfig.isColorBlockVisible()) {
1302            sFoldersPaint.setColor(mHeader.conversation.color);
1303            sFoldersPaint.setStyle(Paint.Style.FILL);
1304            canvas.drawRect(mCoordinates.colorBlockX, mCoordinates.colorBlockY,
1305                    mCoordinates.colorBlockX + mCoordinates.colorBlockWidth,
1306                    mCoordinates.colorBlockY + mCoordinates.colorBlockHeight, sFoldersPaint);
1307        }
1308
1309        // Draw the reply state. Draw nothing if neither replied nor forwarded.
1310        if (mConfig.isReplyStateVisible()) {
1311            if (mHeader.hasBeenRepliedTo && mHeader.hasBeenForwarded) {
1312                canvas.drawBitmap(STATE_REPLIED_AND_FORWARDED, mCoordinates.replyStateX,
1313                        mCoordinates.replyStateY, null);
1314            } else if (mHeader.hasBeenRepliedTo) {
1315                canvas.drawBitmap(STATE_REPLIED, mCoordinates.replyStateX,
1316                        mCoordinates.replyStateY, null);
1317            } else if (mHeader.hasBeenForwarded) {
1318                canvas.drawBitmap(STATE_FORWARDED, mCoordinates.replyStateX,
1319                        mCoordinates.replyStateY, null);
1320            } else if (mHeader.isInvite) {
1321                canvas.drawBitmap(STATE_CALENDAR_INVITE, mCoordinates.replyStateX,
1322                        mCoordinates.replyStateY, null);
1323            }
1324        }
1325
1326        if (mConfig.isPersonalIndicatorVisible()) {
1327            canvas.drawBitmap(mHeader.personalLevelBitmap, mCoordinates.personalIndicatorX,
1328                    mCoordinates.personalIndicatorY, null);
1329        }
1330
1331        // Info icon
1332        if (mHeader.infoIcon != null) {
1333            canvas.drawBitmap(mHeader.infoIcon, mInfoIconX, mCoordinates.infoIconY, sPaint);
1334        }
1335
1336        // Date.
1337        sPaint.setTextSize(mCoordinates.dateFontSize);
1338        sPaint.setTypeface(isUnread ? Typeface.SANS_SERIF : SANS_SERIF_LIGHT);
1339        sPaint.setColor(isUnread ? sDateTextColorUnread : sDateTextColorRead);
1340        drawText(canvas, mHeader.dateText, mDateX, mCoordinates.dateYBaseline, sPaint);
1341
1342        // Paper clip icon.
1343        if (mHeader.paperclip != null) {
1344            canvas.drawBitmap(mHeader.paperclip, mPaperclipX, mCoordinates.paperclipY, sPaint);
1345        }
1346
1347        if (mStarEnabled) {
1348            // Star.
1349            canvas.drawBitmap(getStarBitmap(), mCoordinates.starX, mCoordinates.starY, sPaint);
1350        }
1351
1352        // right-side edge effect when in tablet conversation mode and the list is not collapsed
1353        if (Utils.getDisplayListRightEdgeEffect(mTabletDevice, mListCollapsible,
1354                mConfig.getViewMode())) {
1355            final boolean isRtl = ViewUtils.isViewRtl(this);
1356            RIGHT_EDGE_TABLET.setBounds(
1357                    (isRtl) ? 0 : getWidth() - RIGHT_EDGE_TABLET.getIntrinsicWidth(), 0,
1358                    (isRtl) ? RIGHT_EDGE_TABLET.getIntrinsicWidth() : getWidth(), getHeight());
1359            RIGHT_EDGE_TABLET.draw(canvas);
1360
1361            if (isActivated()) {
1362                final int w = VISIBLE_CONVERSATION_HIGHLIGHT.getIntrinsicWidth();
1363                VISIBLE_CONVERSATION_HIGHLIGHT.setBounds(
1364                        (isRtl) ? getWidth() - w : 0, 0,
1365                        (isRtl) ? getWidth() : w, getHeight());
1366                VISIBLE_CONVERSATION_HIGHLIGHT.draw(canvas);
1367            }
1368        }
1369
1370        // draw the inset divider
1371        sDividerPaint.setColor(sDividerColor);
1372        final int dividerBottomY = getHeight();
1373        final int dividerTopY = dividerBottomY - sDividerHeight;
1374        canvas.drawRect(sDividerInset, dividerTopY, getWidth(), dividerBottomY, sDividerPaint);
1375        Utils.traceEndSection();
1376    }
1377
1378    private void drawSendersImage(final Canvas canvas) {
1379        if (!mSendersImageView.isFlipping()) {
1380            final boolean showSenders = !isSelected();
1381            mSendersImageView.reset(showSenders);
1382        }
1383        canvas.translate(mCoordinates.contactImagesX, mCoordinates.contactImagesY);
1384        if (mPhotoBitmap == null) {
1385            mSendersImageView.draw(canvas);
1386        } else {
1387            canvas.drawBitmap(mPhotoBitmap, null, mPhotoRect, sPaint);
1388        }
1389    }
1390
1391    private void drawSubject(Canvas canvas) {
1392        canvas.translate(mCoordinates.subjectX, mCoordinates.subjectY);
1393        mSubjectTextView.draw(canvas);
1394    }
1395
1396    private void drawSnippet(Canvas canvas) {
1397        // if folders exist, their width will be the max width - actual width
1398        final int folderWidth = mCoordinates.maxSnippetWidth - mSnippetTextView.getWidth();
1399
1400        // in RTL layouts we move the snippet to the right so it doesn't overlap the folders
1401        final int x = mCoordinates.snippetX + (ViewUtils.isViewRtl(this) ? folderWidth : 0);
1402        canvas.translate(x, mCoordinates.snippetY);
1403        mSnippetTextView.draw(canvas);
1404    }
1405
1406    private void drawSenders(Canvas canvas) {
1407        canvas.translate(mSendersX, mCoordinates.sendersY);
1408        mSendersTextView.draw(canvas);
1409    }
1410
1411    private Bitmap getStarBitmap() {
1412        return mHeader.conversation.starred ? STAR_ON : STAR_OFF;
1413    }
1414
1415    private static void drawText(Canvas canvas, CharSequence s, int x, int y, TextPaint paint) {
1416        canvas.drawText(s, 0, s.length(), x, y, paint);
1417    }
1418
1419    /**
1420     * Set the background for this item based on:
1421     * 1. Read / Unread (unread messages have a lighter background)
1422     * 2. Tablet / Phone
1423     * 3. Checkbox checked / Unchecked (controls CAB color for item)
1424     * 4. Activated / Not activated (controls the blue highlight on tablet)
1425     */
1426    private void updateBackground() {
1427        final int background;
1428        if (mBackgroundOverrideResId > 0) {
1429            background = mBackgroundOverrideResId;
1430        } else {
1431            background = R.drawable.conversation_item_background_selector;
1432        }
1433        setBackgroundResource(background);
1434    }
1435
1436    /**
1437     * Toggle the check mark on this view and update the conversation or begin
1438     * drag, if drag is enabled.
1439     */
1440    @Override
1441    public boolean toggleSelectedStateOrBeginDrag() {
1442        ViewMode mode = mActivity.getViewMode();
1443        if (mTabletDevice && mode.isListMode()) {
1444            return beginDragMode();
1445        } else {
1446            return toggleSelectedState("long_press");
1447        }
1448    }
1449
1450    @Override
1451    public boolean toggleSelectedState() {
1452        return toggleSelectedState(null);
1453    }
1454
1455    private boolean toggleSelectedState(final String sourceOpt) {
1456        if (mHeader != null && mHeader.conversation != null && mSelectedConversationSet != null) {
1457            mSelected = !mSelected;
1458            setSelected(mSelected);
1459            final Conversation conv = mHeader.conversation;
1460            // Set the list position of this item in the conversation
1461            final SwipeableListView listView = getListView();
1462
1463            try {
1464                conv.position = mSelected && listView != null ? listView.getPositionForView(this)
1465                        : Conversation.NO_POSITION;
1466            } catch (final NullPointerException e) {
1467                // TODO(skennedy) Remove this if we find the root cause b/9527863
1468            }
1469
1470            if (mSelectedConversationSet.isEmpty()) {
1471                final String source = (sourceOpt != null) ? sourceOpt : "checkbox";
1472                Analytics.getInstance().sendEvent("enter_cab_mode", source, null, 0);
1473            }
1474
1475            mSelectedConversationSet.toggle(conv);
1476            if (mSelectedConversationSet.isEmpty()) {
1477                listView.commitDestructiveActions(true);
1478            }
1479
1480            final boolean front = !mSelected;
1481            mSendersImageView.flipTo(front);
1482
1483            // We update the background after the checked state has changed
1484            // now that we have a selected background asset. Setting the background
1485            // usually waits for a layout pass, but we don't need a full layout,
1486            // just an update to the background.
1487            requestLayout();
1488
1489            return true;
1490        }
1491
1492        return false;
1493    }
1494
1495    @Override
1496    public void onSetEmpty() {
1497        mSendersImageView.flipTo(true);
1498    }
1499
1500    @Override
1501    public void onSetPopulated(final ConversationSelectionSet set) { }
1502
1503    @Override
1504    public void onSetChanged(final ConversationSelectionSet set) { }
1505
1506    /**
1507     * Toggle the star on this view and update the conversation.
1508     */
1509    public void toggleStar() {
1510        mHeader.conversation.starred = !mHeader.conversation.starred;
1511        Bitmap starBitmap = getStarBitmap();
1512        postInvalidate(mCoordinates.starX, mCoordinates.starY, mCoordinates.starX
1513                + starBitmap.getWidth(),
1514                mCoordinates.starY + starBitmap.getHeight());
1515        ConversationCursor cursor = (ConversationCursor) mAdapter.getCursor();
1516        if (cursor != null) {
1517            // TODO(skennedy) What about ads?
1518            cursor.updateBoolean(mHeader.conversation, ConversationColumns.STARRED,
1519                    mHeader.conversation.starred);
1520        }
1521    }
1522
1523    private boolean isTouchInContactPhoto(float x, float y) {
1524        // Everything before the end edge of contact photo
1525
1526        final boolean isRtl = ViewUtils.isViewRtl(this);
1527        final int threshold = (isRtl) ? mCoordinates.contactImagesX - sSenderImageTouchSlop :
1528                mCoordinates.contactImagesX + mCoordinates.contactImagesWidth
1529                + sSenderImageTouchSlop;
1530
1531        // Allow touching a little right of the contact photo when we're already in selection mode
1532        final float extra;
1533        if (mSelectedConversationSet == null || mSelectedConversationSet.isEmpty()) {
1534            extra = 0;
1535        } else {
1536            extra = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16,
1537                    getResources().getDisplayMetrics());
1538        }
1539
1540        return mHeader.gadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO
1541                && ((isRtl) ? x > (threshold - extra) : x < (threshold + extra));
1542    }
1543
1544    private boolean isTouchInInfoIcon(final float x, final float y) {
1545        if (mHeader.infoIcon == null) {
1546            // We have no info icon
1547            return false;
1548        }
1549
1550        final boolean isRtl = ViewUtils.isViewRtl(this);
1551        // Regardless of device, we always want to be end of the date's start touch slop
1552        if (((isRtl) ? x > mDateX + mDateWidth + sStarTouchSlop : x < mDateX - sStarTouchSlop)) {
1553            return false;
1554        }
1555
1556        if (mStarEnabled) {
1557            // We allow touches all the way to the right edge, so no x check is necessary
1558
1559            // We need to be above the star's touch area, which ends at the top of the subject
1560            // text
1561            return y < mCoordinates.subjectY;
1562        }
1563
1564        // With no star below the info icon, we allow touches anywhere from the top edge to the
1565        // bottom edge
1566        return true;
1567    }
1568
1569    private boolean isTouchInStar(float x, float y) {
1570        if (mHeader.infoIcon != null) {
1571            // We have an info icon, and it's above the star
1572            // We allow touches everywhere below the top of the subject text
1573            if (y < mCoordinates.subjectY) {
1574                return false;
1575            }
1576        }
1577
1578        // Everything after the star and include a touch slop.
1579        return mStarEnabled && isTouchInStarTargetX(ViewUtils.isViewRtl(this), x);
1580    }
1581
1582    private boolean isTouchInStarTargetX(boolean isRtl, float x) {
1583        return (isRtl) ? x < mCoordinates.starX + mCoordinates.starWidth + sStarTouchSlop
1584                : x >= mCoordinates.starX - sStarTouchSlop;
1585    }
1586
1587    @Override
1588    public boolean canChildBeDismissed() {
1589        return mSwipeEnabled;
1590    }
1591
1592    @Override
1593    public void dismiss() {
1594        SwipeableListView listView = getListView();
1595        if (listView != null) {
1596            listView.dismissChild(this);
1597        }
1598    }
1599
1600    private boolean onTouchEventNoSwipe(MotionEvent event) {
1601        Utils.traceBeginSection("on touch event no swipe");
1602        boolean handled = false;
1603
1604        int x = (int) event.getX();
1605        int y = (int) event.getY();
1606        mLastTouchX = x;
1607        mLastTouchY = y;
1608        switch (event.getAction()) {
1609            case MotionEvent.ACTION_DOWN:
1610                if (isTouchInContactPhoto(x, y) || isTouchInInfoIcon(x, y) || isTouchInStar(x, y)) {
1611                    mDownEvent = true;
1612                    handled = true;
1613                }
1614                break;
1615
1616            case MotionEvent.ACTION_CANCEL:
1617                mDownEvent = false;
1618                break;
1619
1620            case MotionEvent.ACTION_UP:
1621                if (mDownEvent) {
1622                    if (isTouchInContactPhoto(x, y)) {
1623                        // Touch on the check mark
1624                        toggleSelectedState();
1625                    } else if (isTouchInInfoIcon(x, y)) {
1626                        if (mConversationItemAreaClickListener != null) {
1627                            mConversationItemAreaClickListener.onInfoIconClicked();
1628                        }
1629                    } else if (isTouchInStar(x, y)) {
1630                        // Touch on the star
1631                        if (mConversationItemAreaClickListener == null) {
1632                            toggleStar();
1633                        } else {
1634                            mConversationItemAreaClickListener.onStarClicked();
1635                        }
1636                    }
1637                    handled = true;
1638                }
1639                break;
1640        }
1641
1642        if (!handled) {
1643            handled = super.onTouchEvent(event);
1644        }
1645
1646        Utils.traceEndSection();
1647        return handled;
1648    }
1649
1650    /**
1651     * ConversationItemView is given the first chance to handle touch events.
1652     */
1653    @Override
1654    public boolean onTouchEvent(MotionEvent event) {
1655        Utils.traceBeginSection("on touch event");
1656        int x = (int) event.getX();
1657        int y = (int) event.getY();
1658        mLastTouchX = x;
1659        mLastTouchY = y;
1660        if (!mSwipeEnabled) {
1661            Utils.traceEndSection();
1662            return onTouchEventNoSwipe(event);
1663        }
1664        switch (event.getAction()) {
1665            case MotionEvent.ACTION_DOWN:
1666                if (isTouchInContactPhoto(x, y) || isTouchInInfoIcon(x, y) || isTouchInStar(x, y)) {
1667                    mDownEvent = true;
1668                    Utils.traceEndSection();
1669                    return true;
1670                }
1671                break;
1672            case MotionEvent.ACTION_UP:
1673                if (mDownEvent) {
1674                    if (isTouchInContactPhoto(x, y)) {
1675                        // Touch on the check mark
1676                        Utils.traceEndSection();
1677                        mDownEvent = false;
1678                        toggleSelectedState();
1679                        Utils.traceEndSection();
1680                        return true;
1681                    } else if (isTouchInInfoIcon(x, y)) {
1682                        // Touch on the info icon
1683                        mDownEvent = false;
1684                        if (mConversationItemAreaClickListener != null) {
1685                            mConversationItemAreaClickListener.onInfoIconClicked();
1686                        }
1687                        Utils.traceEndSection();
1688                        return true;
1689                    } else if (isTouchInStar(x, y)) {
1690                        // Touch on the star
1691                        mDownEvent = false;
1692                        if (mConversationItemAreaClickListener == null) {
1693                            toggleStar();
1694                        } else {
1695                            mConversationItemAreaClickListener.onStarClicked();
1696                        }
1697                        Utils.traceEndSection();
1698                        return true;
1699                    }
1700                }
1701                break;
1702        }
1703        // Let View try to handle it as well.
1704        boolean handled = super.onTouchEvent(event);
1705        if (event.getAction() == MotionEvent.ACTION_DOWN) {
1706            Utils.traceEndSection();
1707            return true;
1708        }
1709        Utils.traceEndSection();
1710        return handled;
1711    }
1712
1713    @Override
1714    public boolean performClick() {
1715        final boolean handled = super.performClick();
1716        final SwipeableListView list = getListView();
1717        if (!handled && list != null && list.getAdapter() != null) {
1718            final int pos = list.findConversation(this, mHeader.conversation);
1719            list.performItemClick(this, pos, mHeader.conversation.id);
1720        }
1721        return handled;
1722    }
1723
1724    private View unwrap() {
1725        final ViewParent vp = getParent();
1726        if (vp == null || !(vp instanceof View)) {
1727            return null;
1728        }
1729        return (View) vp;
1730    }
1731
1732    private SwipeableListView getListView() {
1733        SwipeableListView v = null;
1734        final View wrapper = unwrap();
1735        if (wrapper != null && wrapper instanceof SwipeableConversationItemView) {
1736            v = (SwipeableListView) ((SwipeableConversationItemView) wrapper).getListView();
1737        }
1738        if (v == null) {
1739            v = mAdapter.getListView();
1740        }
1741        return v;
1742    }
1743
1744    /**
1745     * Reset any state associated with this conversation item view so that it
1746     * can be reused.
1747     */
1748    public void reset() {
1749        Utils.traceBeginSection("reset");
1750        setAlpha(1f);
1751        setTranslationX(0f);
1752        mAnimatedHeightFraction = 1.0f;
1753        Utils.traceEndSection();
1754    }
1755
1756    @SuppressWarnings("deprecation")
1757    @Override
1758    public void setTranslationX(float translationX) {
1759        super.setTranslationX(translationX);
1760
1761        // When a list item is being swiped or animated, ensure that the hosting view has a
1762        // background color set. We only enable the background during the X-translation effect to
1763        // reduce overdraw during normal list scrolling.
1764        final View parent = (View) getParent();
1765        if (parent == null) {
1766            LogUtils.w(LOG_TAG, "CIV.setTranslationX null ConversationItemView parent x=%s",
1767                    translationX);
1768        }
1769
1770        if (parent instanceof SwipeableConversationItemView) {
1771            if (translationX != 0f) {
1772                parent.setBackgroundResource(R.color.swiped_bg_color);
1773            } else {
1774                parent.setBackgroundDrawable(null);
1775            }
1776        }
1777    }
1778
1779    /**
1780     * Grow the height of the item and fade it in when bringing a conversation
1781     * back from a destructive action.
1782     */
1783    public Animator createSwipeUndoAnimation() {
1784        ObjectAnimator undoAnimator = createTranslateXAnimation(true);
1785        return undoAnimator;
1786    }
1787
1788    /**
1789     * Grow the height of the item and fade it in when bringing a conversation
1790     * back from a destructive action.
1791     */
1792    public Animator createUndoAnimation() {
1793        ObjectAnimator height = createHeightAnimation(true);
1794        Animator fade = ObjectAnimator.ofFloat(this, "alpha", 0, 1.0f);
1795        fade.setDuration(sShrinkAnimationDuration);
1796        fade.setInterpolator(new DecelerateInterpolator(2.0f));
1797        AnimatorSet transitionSet = new AnimatorSet();
1798        transitionSet.playTogether(height, fade);
1799        transitionSet.addListener(new HardwareLayerEnabler(this));
1800        return transitionSet;
1801    }
1802
1803    /**
1804     * Grow the height of the item and fade it in when bringing a conversation
1805     * back from a destructive action.
1806     */
1807    public Animator createDestroyWithSwipeAnimation() {
1808        ObjectAnimator slide = createTranslateXAnimation(false);
1809        ObjectAnimator height = createHeightAnimation(false);
1810        AnimatorSet transitionSet = new AnimatorSet();
1811        transitionSet.playSequentially(slide, height);
1812        return transitionSet;
1813    }
1814
1815    private ObjectAnimator createTranslateXAnimation(boolean show) {
1816        SwipeableListView parent = getListView();
1817        // If we can't get the parent...we have bigger problems.
1818        int width = parent != null ? parent.getMeasuredWidth() : 0;
1819        final float start = show ? width : 0f;
1820        final float end = show ? 0f : width;
1821        ObjectAnimator slide = ObjectAnimator.ofFloat(this, "translationX", start, end);
1822        slide.setInterpolator(new DecelerateInterpolator(2.0f));
1823        slide.setDuration(sSlideAnimationDuration);
1824        return slide;
1825    }
1826
1827    public Animator createDestroyAnimation() {
1828        return createHeightAnimation(false);
1829    }
1830
1831    private ObjectAnimator createHeightAnimation(boolean show) {
1832        final float start = show ? 0f : 1.0f;
1833        final float end = show ? 1.0f : 0f;
1834        ObjectAnimator height = ObjectAnimator.ofFloat(this, "animatedHeightFraction", start, end);
1835        height.setInterpolator(new DecelerateInterpolator(2.0f));
1836        height.setDuration(sShrinkAnimationDuration);
1837        return height;
1838    }
1839
1840    // Used by animator
1841    public void setAnimatedHeightFraction(float height) {
1842        mAnimatedHeightFraction = height;
1843        requestLayout();
1844    }
1845
1846    @Override
1847    public SwipeableView getSwipeableView() {
1848        return SwipeableView.from(this);
1849    }
1850
1851    /**
1852     * Begin drag mode. Keep the conversation selected (NOT toggle selection) and start drag.
1853     */
1854    private boolean beginDragMode() {
1855        if (mLastTouchX < 0 || mLastTouchY < 0 ||  mSelectedConversationSet == null) {
1856            return false;
1857        }
1858        // If this is already checked, don't bother unchecking it!
1859        if (!mSelected) {
1860            toggleSelectedState();
1861        }
1862
1863        // Clip data has form: [conversations_uri, conversationId1,
1864        // maxMessageId1, label1, conversationId2, maxMessageId2, label2, ...]
1865        final int count = mSelectedConversationSet.size();
1866        String description = Utils.formatPlural(mContext, R.plurals.move_conversation, count);
1867
1868        final ClipData data = ClipData.newUri(mContext.getContentResolver(), description,
1869                Conversation.MOVE_CONVERSATIONS_URI);
1870        for (Conversation conversation : mSelectedConversationSet.values()) {
1871            data.addItem(new Item(String.valueOf(conversation.position)));
1872        }
1873        // Protect against non-existent views: only happens for monkeys
1874        final int width = this.getWidth();
1875        final int height = this.getHeight();
1876        final boolean isDimensionNegative = (width < 0) || (height < 0);
1877        if (isDimensionNegative) {
1878            LogUtils.e(LOG_TAG, "ConversationItemView: dimension is negative: "
1879                        + "width=%d, height=%d", width, height);
1880            return false;
1881        }
1882        mActivity.startDragMode();
1883        // Start drag mode
1884        startDrag(data, new ShadowBuilder(this, count, mLastTouchX, mLastTouchY), null, 0);
1885
1886        return true;
1887    }
1888
1889    /**
1890     * Handles the drag event.
1891     *
1892     * @param event the drag event to be handled
1893     */
1894    @Override
1895    public boolean onDragEvent(DragEvent event) {
1896        switch (event.getAction()) {
1897            case DragEvent.ACTION_DRAG_ENDED:
1898                mActivity.stopDragMode();
1899                return true;
1900        }
1901        return false;
1902    }
1903
1904    private class ShadowBuilder extends DragShadowBuilder {
1905        private final Drawable mBackground;
1906
1907        private final View mView;
1908        private final String mDragDesc;
1909        private final int mTouchX;
1910        private final int mTouchY;
1911        private int mDragDescX;
1912        private int mDragDescY;
1913
1914        public ShadowBuilder(View view, int count, int touchX, int touchY) {
1915            super(view);
1916            mView = view;
1917            mBackground = mView.getResources().getDrawable(R.drawable.list_pressed_holo);
1918            mDragDesc = Utils.formatPlural(mView.getContext(), R.plurals.move_conversation, count);
1919            mTouchX = touchX;
1920            mTouchY = touchY;
1921        }
1922
1923        @Override
1924        public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint) {
1925            final int width = mView.getWidth();
1926            final int height = mView.getHeight();
1927
1928            sPaint.setTextSize(mCoordinates.subjectFontSize);
1929            mDragDescX = mCoordinates.sendersX;
1930            mDragDescY = (height - (int) mCoordinates.subjectFontSize) / 2 ;
1931            shadowSize.set(width, height);
1932            shadowTouchPoint.set(mTouchX, mTouchY);
1933        }
1934
1935        @Override
1936        public void onDrawShadow(Canvas canvas) {
1937            mBackground.setBounds(0, 0, mView.getWidth(), mView.getHeight());
1938            mBackground.draw(canvas);
1939            sPaint.setTextSize(mCoordinates.subjectFontSize);
1940            canvas.drawText(mDragDesc, mDragDescX, mDragDescY - sPaint.ascent(), sPaint);
1941        }
1942    }
1943
1944    @Override
1945    public float getMinAllowScrollDistance() {
1946        return sScrollSlop;
1947    }
1948
1949    public String getAccount() {
1950        return mAccount;
1951    }
1952}
1953