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