Home | History | Annotate | Download | only in browse
      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 
     18 package com.android.mail.browse;
     19 
     20 import android.animation.Animator;
     21 import android.animation.AnimatorSet;
     22 import android.animation.ObjectAnimator;
     23 import android.content.BroadcastReceiver;
     24 import android.content.Context;
     25 import android.content.Intent;
     26 import android.content.IntentFilter;
     27 import android.content.res.Resources;
     28 import android.graphics.Bitmap;
     29 import android.graphics.BitmapFactory;
     30 import android.graphics.Canvas;
     31 import android.graphics.Color;
     32 import android.graphics.Paint;
     33 import android.graphics.Rect;
     34 import android.graphics.Typeface;
     35 import android.graphics.drawable.Drawable;
     36 import android.graphics.drawable.InsetDrawable;
     37 import android.support.annotation.Nullable;
     38 import android.support.v4.text.BidiFormatter;
     39 import android.support.v4.text.TextUtilsCompat;
     40 import android.support.v4.view.ViewCompat;
     41 import android.text.Layout.Alignment;
     42 import android.text.Spannable;
     43 import android.text.SpannableString;
     44 import android.text.SpannableStringBuilder;
     45 import android.text.StaticLayout;
     46 import android.text.TextPaint;
     47 import android.text.TextUtils;
     48 import android.text.TextUtils.TruncateAt;
     49 import android.text.format.DateUtils;
     50 import android.text.style.BackgroundColorSpan;
     51 import android.text.style.CharacterStyle;
     52 import android.text.style.ForegroundColorSpan;
     53 import android.text.style.TextAppearanceSpan;
     54 import android.util.SparseArray;
     55 import android.util.TypedValue;
     56 import android.view.MotionEvent;
     57 import android.view.View;
     58 import android.view.ViewGroup;
     59 import android.view.ViewParent;
     60 import android.view.animation.DecelerateInterpolator;
     61 import android.widget.TextView;
     62 
     63 import com.android.mail.R;
     64 import com.android.mail.analytics.Analytics;
     65 import com.android.mail.bitmap.CheckableContactFlipDrawable;
     66 import com.android.mail.bitmap.ContactDrawable;
     67 import com.android.mail.perf.Timer;
     68 import com.android.mail.providers.Account;
     69 import com.android.mail.providers.Conversation;
     70 import com.android.mail.providers.Folder;
     71 import com.android.mail.providers.UIProvider;
     72 import com.android.mail.providers.UIProvider.ConversationColumns;
     73 import com.android.mail.providers.UIProvider.ConversationListIcon;
     74 import com.android.mail.providers.UIProvider.FolderType;
     75 import com.android.mail.ui.AnimatedAdapter;
     76 import com.android.mail.ui.ControllableActivity;
     77 import com.android.mail.ui.ConversationCheckedSet;
     78 import com.android.mail.ui.ConversationSetObserver;
     79 import com.android.mail.ui.FolderDisplayer;
     80 import com.android.mail.ui.SwipeableItemView;
     81 import com.android.mail.ui.SwipeableListView;
     82 import com.android.mail.utils.FolderUri;
     83 import com.android.mail.utils.HardwareLayerEnabler;
     84 import com.android.mail.utils.LogTag;
     85 import com.android.mail.utils.LogUtils;
     86 import com.android.mail.utils.Utils;
     87 import com.android.mail.utils.ViewUtils;
     88 import com.google.common.annotations.VisibleForTesting;
     89 
     90 import java.util.List;
     91 import java.util.Locale;
     92 
     93 public 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