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