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 FOCUSED_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            FOCUSED_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        final SwipeableListView listView = getListView();
1277        if (listView != null && listView.isConversationSelected(getConversation())) {
1278            final int w = FOCUSED_CONVERSATION_HIGHLIGHT.getIntrinsicWidth();
1279            final boolean isRtl = ViewUtils.isViewRtl(this);
1280            // This bar is on the right side of the conv list if it's RTL
1281            FOCUSED_CONVERSATION_HIGHLIGHT.setBounds(
1282                    (isRtl) ? getWidth() - w : 0, 0,
1283                    (isRtl) ? getWidth() : w, getHeight());
1284            FOCUSED_CONVERSATION_HIGHLIGHT.draw(canvas);
1285        }
1286
1287        Utils.traceEndSection();
1288    }
1289
1290    @Override
1291    public void setSelected(boolean selected) {
1292        // We catch the selected event here instead of using ListView#setOnItemSelectedListener
1293        // because when the framework changes selection due to keyboard events, it sets the selected
1294        // state, re-draw the affected views, and then call onItemSelected. That approach won't work
1295        // because the view won't know about the new selected position during the re-draw.
1296        if (selected) {
1297            final SwipeableListView listView = getListView();
1298            if (listView != null) {
1299                listView.setSelectedConversation(getConversation());
1300            }
1301        }
1302        super.setSelected(selected);
1303    }
1304
1305    private void drawSendersImage(final Canvas canvas) {
1306        if (!mSendersImageView.isFlipping()) {
1307            final boolean showSenders = !mChecked;
1308            mSendersImageView.reset(showSenders);
1309        }
1310        canvas.translate(mCoordinates.contactImagesX, mCoordinates.contactImagesY);
1311        if (mPhotoBitmap == null) {
1312            mSendersImageView.draw(canvas);
1313        } else {
1314            canvas.drawBitmap(mPhotoBitmap, null, mPhotoRect, sPaint);
1315        }
1316    }
1317
1318    private void drawSubject(Canvas canvas) {
1319        canvas.translate(mCoordinates.subjectX, mCoordinates.subjectY);
1320        mSubjectTextView.draw(canvas);
1321    }
1322
1323    private void drawSnippet(Canvas canvas) {
1324        // if folders exist, their width will be the max width - actual width
1325        final int folderWidth = mCoordinates.maxSnippetWidth - mSnippetTextView.getWidth();
1326
1327        // in RTL layouts we move the snippet to the right so it doesn't overlap the folders
1328        final int x = mCoordinates.snippetX + (ViewUtils.isViewRtl(this) ? folderWidth : 0);
1329        canvas.translate(x, mCoordinates.snippetY);
1330        mSnippetTextView.draw(canvas);
1331    }
1332
1333    private void drawSenders(Canvas canvas) {
1334        canvas.translate(mSendersX, mCoordinates.sendersY);
1335        mSendersTextView.draw(canvas);
1336    }
1337
1338    private Bitmap getStarBitmap() {
1339        return mHeader.conversation.starred ? STAR_ON : STAR_OFF;
1340    }
1341
1342    private static void drawText(Canvas canvas, CharSequence s, int x, int y, TextPaint paint) {
1343        canvas.drawText(s, 0, s.length(), x, y, paint);
1344    }
1345
1346    /**
1347     * Set the background for this item based on:
1348     * 1. Read / Unread (unread messages have a lighter background)
1349     * 2. Tablet / Phone
1350     * 3. Checkbox checked / Unchecked (controls CAB color for item)
1351     * 4. Activated / Not activated (controls the blue highlight on tablet)
1352     */
1353    private void updateBackground() {
1354        final int background;
1355        if (mBackgroundOverrideResId > 0) {
1356            background = mBackgroundOverrideResId;
1357        } else {
1358            background = R.drawable.conversation_item_background;
1359        }
1360        setBackgroundResource(background);
1361    }
1362
1363    @Override
1364    protected int[] onCreateDrawableState(int extraSpace) {
1365        final int[] curr = super.onCreateDrawableState(extraSpace + 1);
1366        if (mChecked) {
1367            mergeDrawableStates(curr, CHECKED_STATE);
1368        }
1369        return curr;
1370    }
1371
1372    private void setChecked(boolean checked) {
1373        mChecked = checked;
1374        refreshDrawableState();
1375    }
1376
1377    @Override
1378    public boolean toggleCheckedState() {
1379        return toggleCheckedState(null);
1380    }
1381
1382    @Override
1383    public boolean toggleCheckedState(final String sourceOpt) {
1384        if (mHeader != null && mHeader.conversation != null && mCheckedConversationSet != null) {
1385            setChecked(!mChecked);
1386            final Conversation conv = mHeader.conversation;
1387            // Set the list position of this item in the conversation
1388            final SwipeableListView listView = getListView();
1389
1390            try {
1391                conv.position = mChecked && listView != null ? listView.getPositionForView(this)
1392                        : Conversation.NO_POSITION;
1393            } catch (final NullPointerException e) {
1394                // TODO(skennedy) Remove this if we find the root cause b/9527863
1395            }
1396
1397            if (mCheckedConversationSet.isEmpty()) {
1398                final String source = (sourceOpt != null) ? sourceOpt : "checkbox";
1399                Analytics.getInstance().sendEvent("enter_cab_mode", source, null, 0);
1400            }
1401
1402            mCheckedConversationSet.toggle(conv);
1403            if (mCheckedConversationSet.isEmpty()) {
1404                listView.commitDestructiveActions(true);
1405            }
1406
1407            final boolean front = !mChecked;
1408            mSendersImageView.flipTo(front);
1409
1410            // We update the background after the checked state has changed
1411            // now that we have a selected background asset. Setting the background
1412            // usually waits for a layout pass, but we don't need a full layout,
1413            // just an update to the background.
1414            requestLayout();
1415
1416            return true;
1417        }
1418
1419        return false;
1420    }
1421
1422    @Override
1423    public void onSetEmpty() {
1424        mSendersImageView.flipTo(true);
1425    }
1426
1427    @Override
1428    public void onSetPopulated(final ConversationCheckedSet set) { }
1429
1430    @Override
1431    public void onSetChanged(final ConversationCheckedSet set) { }
1432
1433    /**
1434     * Toggle the star on this view and update the conversation.
1435     */
1436    public void toggleStar() {
1437        mHeader.conversation.starred = !mHeader.conversation.starred;
1438        Bitmap starBitmap = getStarBitmap();
1439        postInvalidate(mCoordinates.starX, mCoordinates.starY, mCoordinates.starX
1440                + starBitmap.getWidth(),
1441                mCoordinates.starY + starBitmap.getHeight());
1442        ConversationCursor cursor = (ConversationCursor) mAdapter.getCursor();
1443        if (cursor != null) {
1444            // TODO(skennedy) What about ads?
1445            cursor.updateBoolean(mHeader.conversation, ConversationColumns.STARRED,
1446                    mHeader.conversation.starred);
1447        }
1448    }
1449
1450    private boolean isTouchInContactPhoto(float x, float y) {
1451        // Everything before the end edge of contact photo
1452
1453        final boolean isRtl = ViewUtils.isViewRtl(this);
1454        final int threshold = (isRtl) ? mCoordinates.contactImagesX - sSenderImageTouchSlop :
1455                mCoordinates.contactImagesX + mCoordinates.contactImagesWidth
1456                + sSenderImageTouchSlop;
1457
1458        // Allow touching a little right of the contact photo when we're already in selection mode
1459        final float extra;
1460        if (mCheckedConversationSet == null || mCheckedConversationSet.isEmpty()) {
1461            extra = 0;
1462        } else {
1463            extra = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16,
1464                    getResources().getDisplayMetrics());
1465        }
1466
1467        return mHeader.gadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO
1468                && ((isRtl) ? x > (threshold - extra) : x < (threshold + extra));
1469    }
1470
1471    private boolean isTouchInInfoIcon(final float x, final float y) {
1472        if (mHeader.infoIcon == null) {
1473            // We have no info icon
1474            return false;
1475        }
1476
1477        final boolean isRtl = ViewUtils.isViewRtl(this);
1478        // Regardless of device, we always want to be end of the date's start touch slop
1479        if (((isRtl) ? x > mDateX + mDateWidth + sStarTouchSlop : x < mDateX - sStarTouchSlop)) {
1480            return false;
1481        }
1482
1483        if (mStarEnabled) {
1484            // We allow touches all the way to the right edge, so no x check is necessary
1485
1486            // We need to be above the star's touch area, which ends at the top of the subject
1487            // text
1488            return y < mCoordinates.subjectY;
1489        }
1490
1491        // With no star below the info icon, we allow touches anywhere from the top edge to the
1492        // bottom edge
1493        return true;
1494    }
1495
1496    private boolean isTouchInStar(float x, float y) {
1497        if (mHeader.infoIcon != null) {
1498            // We have an info icon, and it's above the star
1499            // We allow touches everywhere below the top of the subject text
1500            if (y < mCoordinates.subjectY) {
1501                return false;
1502            }
1503        }
1504
1505        // Everything after the star and include a touch slop.
1506        return mStarEnabled && isTouchInStarTargetX(ViewUtils.isViewRtl(this), x);
1507    }
1508
1509    private boolean isTouchInStarTargetX(boolean isRtl, float x) {
1510        return (isRtl) ? x < mCoordinates.starX + mCoordinates.starWidth + sStarTouchSlop
1511                : x >= mCoordinates.starX - sStarTouchSlop;
1512    }
1513
1514    @Override
1515    public boolean canChildBeDismissed() {
1516        return mSwipeEnabled;
1517    }
1518
1519    @Override
1520    public void dismiss() {
1521        SwipeableListView listView = getListView();
1522        if (listView != null) {
1523            listView.dismissChild(this);
1524        }
1525    }
1526
1527    private boolean onTouchEventNoSwipe(MotionEvent event) {
1528        Utils.traceBeginSection("on touch event no swipe");
1529        boolean handled = false;
1530
1531        int x = (int) event.getX();
1532        int y = (int) event.getY();
1533        switch (event.getAction()) {
1534            case MotionEvent.ACTION_DOWN:
1535                if (isTouchInContactPhoto(x, y) || isTouchInInfoIcon(x, y) || isTouchInStar(x, y)) {
1536                    mDownEvent = true;
1537                    handled = true;
1538                }
1539                break;
1540
1541            case MotionEvent.ACTION_CANCEL:
1542                mDownEvent = false;
1543                break;
1544
1545            case MotionEvent.ACTION_UP:
1546                if (mDownEvent) {
1547                    if (isTouchInContactPhoto(x, y)) {
1548                        // Touch on the check mark
1549                        toggleCheckedState();
1550                    } else if (isTouchInInfoIcon(x, y)) {
1551                        if (mConversationItemAreaClickListener != null) {
1552                            mConversationItemAreaClickListener.onInfoIconClicked();
1553                        }
1554                    } else if (isTouchInStar(x, y)) {
1555                        // Touch on the star
1556                        if (mConversationItemAreaClickListener == null) {
1557                            toggleStar();
1558                        } else {
1559                            mConversationItemAreaClickListener.onStarClicked();
1560                        }
1561                    }
1562                    handled = true;
1563                }
1564                break;
1565        }
1566
1567        if (!handled) {
1568            handled = super.onTouchEvent(event);
1569        }
1570
1571        Utils.traceEndSection();
1572        return handled;
1573    }
1574
1575    /**
1576     * ConversationItemView is given the first chance to handle touch events.
1577     */
1578    @Override
1579    public boolean onTouchEvent(MotionEvent event) {
1580        Utils.traceBeginSection("on touch event");
1581        int x = (int) event.getX();
1582        int y = (int) event.getY();
1583        if (!mSwipeEnabled) {
1584            Utils.traceEndSection();
1585            return onTouchEventNoSwipe(event);
1586        }
1587        switch (event.getAction()) {
1588            case MotionEvent.ACTION_DOWN:
1589                if (isTouchInContactPhoto(x, y) || isTouchInInfoIcon(x, y) || isTouchInStar(x, y)) {
1590                    mDownEvent = true;
1591                    Utils.traceEndSection();
1592                    return true;
1593                }
1594                break;
1595            case MotionEvent.ACTION_UP:
1596                if (mDownEvent) {
1597                    if (isTouchInContactPhoto(x, y)) {
1598                        // Touch on the check mark
1599                        Utils.traceEndSection();
1600                        mDownEvent = false;
1601                        toggleCheckedState();
1602                        Utils.traceEndSection();
1603                        return true;
1604                    } else if (isTouchInInfoIcon(x, y)) {
1605                        // Touch on the info icon
1606                        mDownEvent = false;
1607                        if (mConversationItemAreaClickListener != null) {
1608                            mConversationItemAreaClickListener.onInfoIconClicked();
1609                        }
1610                        Utils.traceEndSection();
1611                        return true;
1612                    } else if (isTouchInStar(x, y)) {
1613                        // Touch on the star
1614                        mDownEvent = false;
1615                        if (mConversationItemAreaClickListener == null) {
1616                            toggleStar();
1617                        } else {
1618                            mConversationItemAreaClickListener.onStarClicked();
1619                        }
1620                        Utils.traceEndSection();
1621                        return true;
1622                    }
1623                }
1624                break;
1625        }
1626        // Let View try to handle it as well.
1627        boolean handled = super.onTouchEvent(event);
1628        if (event.getAction() == MotionEvent.ACTION_DOWN) {
1629            Utils.traceEndSection();
1630            return true;
1631        }
1632        Utils.traceEndSection();
1633        return handled;
1634    }
1635
1636    @Override
1637    public boolean performClick() {
1638        final boolean handled = super.performClick();
1639        final SwipeableListView list = getListView();
1640        if (!handled && list != null && list.getAdapter() != null) {
1641            final int pos = list.findConversation(this, mHeader.conversation);
1642            list.performItemClick(this, pos, mHeader.conversation.id);
1643        }
1644        return handled;
1645    }
1646
1647    private View unwrap() {
1648        final ViewParent vp = getParent();
1649        if (vp == null || !(vp instanceof View)) {
1650            return null;
1651        }
1652        return (View) vp;
1653    }
1654
1655    private SwipeableListView getListView() {
1656        SwipeableListView v = null;
1657        final View wrapper = unwrap();
1658        if (wrapper != null && wrapper instanceof SwipeableConversationItemView) {
1659            v = (SwipeableListView) ((SwipeableConversationItemView) wrapper).getListView();
1660        }
1661        if (v == null) {
1662            v = mAdapter.getListView();
1663        }
1664        return v;
1665    }
1666
1667    /**
1668     * Reset any state associated with this conversation item view so that it
1669     * can be reused.
1670     */
1671    public void reset() {
1672        Utils.traceBeginSection("reset");
1673        setAlpha(1f);
1674        setTranslationX(0f);
1675        mAnimatedHeightFraction = 1.0f;
1676        Utils.traceEndSection();
1677    }
1678
1679    @SuppressWarnings("deprecation")
1680    @Override
1681    public void setTranslationX(float translationX) {
1682        super.setTranslationX(translationX);
1683
1684        // When a list item is being swiped or animated, ensure that the hosting view has a
1685        // background color set. We only enable the background during the X-translation effect to
1686        // reduce overdraw during normal list scrolling.
1687        final View parent = (View) getParent();
1688        if (parent == null) {
1689            LogUtils.w(LOG_TAG, "CIV.setTranslationX null ConversationItemView parent x=%s",
1690                    translationX);
1691        }
1692
1693        if (parent instanceof SwipeableConversationItemView) {
1694            if (translationX != 0f) {
1695                parent.setBackgroundResource(R.color.swiped_bg_color);
1696            } else {
1697                parent.setBackgroundDrawable(null);
1698            }
1699        }
1700    }
1701
1702    /**
1703     * Grow the height of the item and fade it in when bringing a conversation
1704     * back from a destructive action.
1705     */
1706    public Animator createSwipeUndoAnimation() {
1707        ObjectAnimator undoAnimator = createTranslateXAnimation(true);
1708        return undoAnimator;
1709    }
1710
1711    /**
1712     * Grow the height of the item and fade it in when bringing a conversation
1713     * back from a destructive action.
1714     */
1715    public Animator createUndoAnimation() {
1716        ObjectAnimator height = createHeightAnimation(true);
1717        Animator fade = ObjectAnimator.ofFloat(this, "alpha", 0, 1.0f);
1718        fade.setDuration(sShrinkAnimationDuration);
1719        fade.setInterpolator(new DecelerateInterpolator(2.0f));
1720        AnimatorSet transitionSet = new AnimatorSet();
1721        transitionSet.playTogether(height, fade);
1722        transitionSet.addListener(new HardwareLayerEnabler(this));
1723        return transitionSet;
1724    }
1725
1726    /**
1727     * Grow the height of the item and fade it in when bringing a conversation
1728     * back from a destructive action.
1729     */
1730    public Animator createDestroyWithSwipeAnimation() {
1731        ObjectAnimator slide = createTranslateXAnimation(false);
1732        ObjectAnimator height = createHeightAnimation(false);
1733        AnimatorSet transitionSet = new AnimatorSet();
1734        transitionSet.playSequentially(slide, height);
1735        return transitionSet;
1736    }
1737
1738    private ObjectAnimator createTranslateXAnimation(boolean show) {
1739        SwipeableListView parent = getListView();
1740        // If we can't get the parent...we have bigger problems.
1741        int width = parent != null ? parent.getMeasuredWidth() : 0;
1742        final float start = show ? width : 0f;
1743        final float end = show ? 0f : width;
1744        ObjectAnimator slide = ObjectAnimator.ofFloat(this, "translationX", start, end);
1745        slide.setInterpolator(new DecelerateInterpolator(2.0f));
1746        slide.setDuration(sSlideAnimationDuration);
1747        return slide;
1748    }
1749
1750    public Animator createDestroyAnimation() {
1751        return createHeightAnimation(false);
1752    }
1753
1754    private ObjectAnimator createHeightAnimation(boolean show) {
1755        final float start = show ? 0f : 1.0f;
1756        final float end = show ? 1.0f : 0f;
1757        ObjectAnimator height = ObjectAnimator.ofFloat(this, "animatedHeightFraction", start, end);
1758        height.setInterpolator(new DecelerateInterpolator(2.0f));
1759        height.setDuration(sShrinkAnimationDuration);
1760        return height;
1761    }
1762
1763    // Used by animator
1764    public void setAnimatedHeightFraction(float height) {
1765        mAnimatedHeightFraction = height;
1766        requestLayout();
1767    }
1768
1769    @Override
1770    public SwipeableView getSwipeableView() {
1771        return SwipeableView.from(this);
1772    }
1773
1774    @Override
1775    public float getMinAllowScrollDistance() {
1776        return sScrollSlop;
1777    }
1778
1779    public String getAccountEmailAddress() {
1780        return mAccount.getEmailAddress();
1781    }
1782}
1783