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