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