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