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.Animator.AnimatorListener;
     22 import android.animation.AnimatorListenerAdapter;
     23 import android.animation.AnimatorSet;
     24 import android.animation.ObjectAnimator;
     25 import android.content.ClipData;
     26 import android.content.ClipData.Item;
     27 import android.content.Context;
     28 import android.content.res.Resources;
     29 import android.graphics.Bitmap;
     30 import android.graphics.BitmapFactory;
     31 import android.graphics.Canvas;
     32 import android.graphics.Color;
     33 import android.graphics.LinearGradient;
     34 import android.graphics.Matrix;
     35 import android.graphics.Paint;
     36 import android.graphics.Point;
     37 import android.graphics.Rect;
     38 import android.graphics.Shader;
     39 import android.graphics.Typeface;
     40 import android.graphics.drawable.Drawable;
     41 import android.text.Layout.Alignment;
     42 import android.text.Spannable;
     43 import android.text.SpannableString;
     44 import android.text.SpannableStringBuilder;
     45 import android.text.StaticLayout;
     46 import android.text.TextPaint;
     47 import android.text.TextUtils;
     48 import android.text.TextUtils.TruncateAt;
     49 import android.text.format.DateUtils;
     50 import android.text.style.CharacterStyle;
     51 import android.text.style.ForegroundColorSpan;
     52 import android.text.style.TextAppearanceSpan;
     53 import android.text.util.Rfc822Token;
     54 import android.text.util.Rfc822Tokenizer;
     55 import android.util.SparseArray;
     56 import android.util.TypedValue;
     57 import android.view.DragEvent;
     58 import android.view.MotionEvent;
     59 import android.view.View;
     60 import android.view.ViewGroup;
     61 import android.view.ViewParent;
     62 import android.view.animation.DecelerateInterpolator;
     63 import android.view.animation.LinearInterpolator;
     64 import android.widget.AbsListView;
     65 import android.widget.AbsListView.OnScrollListener;
     66 import android.widget.TextView;
     67 
     68 import com.android.mail.R;
     69 import com.android.mail.R.drawable;
     70 import com.android.mail.R.integer;
     71 import com.android.mail.analytics.Analytics;
     72 import com.android.mail.bitmap.AttachmentDrawable;
     73 import com.android.mail.bitmap.AttachmentGridDrawable;
     74 import com.android.mail.browse.ConversationItemViewModel.SenderFragment;
     75 import com.android.mail.perf.Timer;
     76 import com.android.mail.photomanager.ContactPhotoManager;
     77 import com.android.mail.photomanager.ContactPhotoManager.ContactIdentifier;
     78 import com.android.mail.photomanager.PhotoManager.PhotoIdentifier;
     79 import com.android.mail.providers.Address;
     80 import com.android.mail.providers.Attachment;
     81 import com.android.mail.providers.Conversation;
     82 import com.android.mail.providers.Folder;
     83 import com.android.mail.providers.UIProvider;
     84 import com.android.mail.providers.UIProvider.AttachmentRendition;
     85 import com.android.mail.providers.UIProvider.ConversationColumns;
     86 import com.android.mail.providers.UIProvider.ConversationListIcon;
     87 import com.android.mail.providers.UIProvider.FolderType;
     88 import com.android.mail.ui.AnimatedAdapter;
     89 import com.android.mail.ui.AnimatedAdapter.ConversationListListener;
     90 import com.android.mail.ui.ControllableActivity;
     91 import com.android.mail.ui.ConversationSelectionSet;
     92 import com.android.mail.ui.DividedImageCanvas;
     93 import com.android.mail.ui.DividedImageCanvas.InvalidateCallback;
     94 import com.android.mail.ui.FolderDisplayer;
     95 import com.android.mail.ui.SwipeableItemView;
     96 import com.android.mail.ui.SwipeableListView;
     97 import com.android.mail.ui.ViewMode;
     98 import com.android.mail.utils.FolderUri;
     99 import com.android.mail.utils.HardwareLayerEnabler;
    100 import com.android.mail.utils.LogTag;
    101 import com.android.mail.utils.LogUtils;
    102 import com.android.mail.utils.Utils;
    103 import com.google.common.annotations.VisibleForTesting;
    104 import com.google.common.collect.Lists;
    105 
    106 import java.util.ArrayList;
    107 import java.util.List;
    108 
    109 public class ConversationItemView extends View
    110         implements SwipeableItemView, ToggleableItem, InvalidateCallback, OnScrollListener {
    111 
    112     // Timer.
    113     private static int sLayoutCount = 0;
    114     private static Timer sTimer; // Create the sTimer here if you need to do
    115                                  // perf analysis.
    116     private static final int PERF_LAYOUT_ITERATIONS = 50;
    117     private static final String PERF_TAG_LAYOUT = "CCHV.layout";
    118     private static final String PERF_TAG_CALCULATE_TEXTS_BITMAPS = "CCHV.txtsbmps";
    119     private static final String PERF_TAG_CALCULATE_SENDER_SUBJECT = "CCHV.sendersubj";
    120     private static final String PERF_TAG_CALCULATE_FOLDERS = "CCHV.folders";
    121     private static final String PERF_TAG_CALCULATE_COORDINATES = "CCHV.coordinates";
    122     private static final String LOG_TAG = LogTag.getLogTag();
    123 
    124     // Static bitmaps.
    125     private static Bitmap STAR_OFF;
    126     private static Bitmap STAR_ON;
    127     private static Bitmap CHECK;
    128     private static Bitmap ATTACHMENT;
    129     private static Bitmap ONLY_TO_ME;
    130     private static Bitmap TO_ME_AND_OTHERS;
    131     private static Bitmap IMPORTANT_ONLY_TO_ME;
    132     private static Bitmap IMPORTANT_TO_ME_AND_OTHERS;
    133     private static Bitmap IMPORTANT_TO_OTHERS;
    134     private static Bitmap STATE_REPLIED;
    135     private static Bitmap STATE_FORWARDED;
    136     private static Bitmap STATE_REPLIED_AND_FORWARDED;
    137     private static Bitmap STATE_CALENDAR_INVITE;
    138     private static Bitmap VISIBLE_CONVERSATION_CARET;
    139     private static Drawable RIGHT_EDGE_TABLET;
    140     private static Drawable PLACEHOLDER;
    141     private static Drawable PROGRESS_BAR;
    142 
    143     private static String sSendersSplitToken;
    144     private static String sElidedPaddingToken;
    145 
    146     // Static colors.
    147     private static int sSendersTextColorRead;
    148     private static int sSendersTextColorUnread;
    149     private static int sDateTextColor;
    150     private static int sStarTouchSlop;
    151     private static int sSenderImageTouchSlop;
    152     private static int sShrinkAnimationDuration;
    153     private static int sSlideAnimationDuration;
    154     private static int sOverflowCountMax;
    155     private static int sCabAnimationDuration;
    156 
    157     // Static paints.
    158     private static final TextPaint sPaint = new TextPaint();
    159     private static final TextPaint sFoldersPaint = new TextPaint();
    160     private static final Paint sCheckBackgroundPaint = new Paint();
    161 
    162     // Backgrounds for different states.
    163     private final SparseArray<Drawable> mBackgrounds = new SparseArray<Drawable>();
    164 
    165     // Dimensions and coordinates.
    166     private int mViewWidth = -1;
    167     /** The view mode at which we calculated mViewWidth previously. */
    168     private int mPreviousMode;
    169 
    170     private int mInfoIconX;
    171     private int mDateX;
    172     private int mPaperclipX;
    173     private int mSendersWidth;
    174 
    175     /** Whether we are on a tablet device or not */
    176     private final boolean mTabletDevice;
    177     /** Whether we are on an expansive tablet */
    178     private final boolean mIsExpansiveTablet;
    179     /** When in conversation mode, true if the list is hidden */
    180     private final boolean mListCollapsible;
    181 
    182     @VisibleForTesting
    183     ConversationItemViewCoordinates mCoordinates;
    184 
    185     private ConversationItemViewCoordinates.Config mConfig;
    186 
    187     private final Context mContext;
    188 
    189     public ConversationItemViewModel mHeader;
    190     private boolean mDownEvent;
    191     private boolean mSelected = false;
    192     private ConversationSelectionSet mSelectedConversationSet;
    193     private Folder mDisplayedFolder;
    194     private boolean mStarEnabled;
    195     private boolean mSwipeEnabled;
    196     private int mLastTouchX;
    197     private int mLastTouchY;
    198     private AnimatedAdapter mAdapter;
    199     private float mAnimatedHeightFraction = 1.0f;
    200     private final String mAccount;
    201     private ControllableActivity mActivity;
    202     private ConversationListListener mConversationListListener;
    203     private final TextView mSubjectTextView;
    204     private final TextView mSendersTextView;
    205     private int mGadgetMode;
    206     private boolean mAttachmentPreviewsEnabled;
    207     private boolean mParallaxSpeedAlternative;
    208     private boolean mParallaxDirectionAlternative;
    209     private final DividedImageCanvas mContactImagesHolder;
    210     private static ContactPhotoManager sContactPhotoManager;
    211 
    212     private static int sFoldersLeftPadding;
    213     private static TextAppearanceSpan sSubjectTextUnreadSpan;
    214     private static TextAppearanceSpan sSubjectTextReadSpan;
    215     private static ForegroundColorSpan sSnippetTextUnreadSpan;
    216     private static ForegroundColorSpan sSnippetTextReadSpan;
    217     private static int sScrollSlop;
    218     private static CharacterStyle sActivatedTextSpan;
    219 
    220     private final AttachmentGridDrawable mAttachmentsView;
    221 
    222     private final Matrix mPhotoFlipMatrix = new Matrix();
    223     private final Matrix mCheckMatrix = new Matrix();
    224 
    225     private final CabAnimator mPhotoFlipAnimator;
    226 
    227     /**
    228      * The conversation id, if this conversation was selected the last time we were in a selection
    229      * mode. This is reset after any animations complete upon exiting the selection mode.
    230      */
    231     private long mLastSelectedId = -1;
    232 
    233     /** The resource id of the color to use to override the background. */
    234     private int mBackgroundOverrideResId = -1;
    235     /** The bitmap to use, or <code>null</code> for the default */
    236     private Bitmap mPhotoBitmap = null;
    237     private Rect mPhotoRect = null;
    238 
    239     /**
    240      * A listener for clicks on the various areas of a conversation item.
    241      */
    242     public interface ConversationItemAreaClickListener {
    243         /** Called when the info icon is clicked. */
    244         void onInfoIconClicked();
    245 
    246         /** Called when the star is clicked. */
    247         void onStarClicked();
    248     }
    249 
    250     /** If set, it will steal all clicks for which the interface has a click method. */
    251     private ConversationItemAreaClickListener mConversationItemAreaClickListener = null;
    252 
    253     static {
    254         sPaint.setAntiAlias(true);
    255         sFoldersPaint.setAntiAlias(true);
    256 
    257         sCheckBackgroundPaint.setColor(Color.GRAY);
    258     }
    259 
    260     public static void setScrollStateChanged(final int scrollState) {
    261         if (sContactPhotoManager == null) {
    262             return;
    263         }
    264         final boolean flinging = scrollState == OnScrollListener.SCROLL_STATE_FLING;
    265 
    266         if (flinging) {
    267             sContactPhotoManager.pause();
    268         } else {
    269             sContactPhotoManager.resume();
    270         }
    271     }
    272 
    273     /**
    274      * Handles displaying folders in a conversation header view.
    275      */
    276     static class ConversationItemFolderDisplayer extends FolderDisplayer {
    277 
    278         private int mFoldersCount;
    279 
    280         public ConversationItemFolderDisplayer(Context context) {
    281             super(context);
    282         }
    283 
    284         @Override
    285         public void loadConversationFolders(Conversation conv, final FolderUri ignoreFolderUri,
    286                 final int ignoreFolderType) {
    287             super.loadConversationFolders(conv, ignoreFolderUri, ignoreFolderType);
    288             mFoldersCount = mFoldersSortedSet.size();
    289         }
    290 
    291         @Override
    292         public void reset() {
    293             super.reset();
    294             mFoldersCount = 0;
    295         }
    296 
    297         public boolean hasVisibleFolders() {
    298             return mFoldersCount > 0;
    299         }
    300 
    301         private int measureFolders(int availableSpace, int cellSize) {
    302             int totalWidth = 0;
    303             boolean firstTime = true;
    304             for (Folder f : mFoldersSortedSet) {
    305                 final String folderString = f.name;
    306                 int width = (int) sFoldersPaint.measureText(folderString) + cellSize;
    307                 if (firstTime) {
    308                     firstTime = false;
    309                 } else {
    310                     width += sFoldersLeftPadding;
    311                 }
    312                 totalWidth += width;
    313                 if (totalWidth > availableSpace) {
    314                     break;
    315                 }
    316             }
    317 
    318             return totalWidth;
    319         }
    320 
    321         public void drawFolders(Canvas canvas, ConversationItemViewCoordinates coordinates) {
    322             if (mFoldersCount == 0) {
    323                 return;
    324             }
    325             final int xMinStart = coordinates.foldersX;
    326             final int xEnd = coordinates.foldersXEnd;
    327             final int y = coordinates.foldersY;
    328             final int height = coordinates.foldersHeight;
    329             int textBottomPadding = coordinates.foldersTextBottomPadding;
    330 
    331             sFoldersPaint.setTextSize(coordinates.foldersFontSize);
    332             sFoldersPaint.setTypeface(coordinates.foldersTypeface);
    333 
    334             // Initialize space and cell size based on the current mode.
    335             int availableSpace = xEnd - xMinStart;
    336             int maxFoldersCount = availableSpace / coordinates.getFolderMinimumWidth();
    337             int foldersCount = Math.min(mFoldersCount, maxFoldersCount);
    338             int averageWidth = availableSpace / foldersCount;
    339             int cellSize = coordinates.getFolderCellWidth();
    340 
    341             // TODO(ath): sFoldersPaint.measureText() is done 3x in this method. stop that.
    342             // Extra credit: maybe cache results across items as long as font size doesn't change.
    343 
    344             final int totalWidth = measureFolders(availableSpace, cellSize);
    345             int xStart = xEnd - Math.min(availableSpace, totalWidth);
    346             final boolean overflow = totalWidth > availableSpace;
    347 
    348             // Second pass to draw folders.
    349             int i = 0;
    350             for (Folder f : mFoldersSortedSet) {
    351                 if (availableSpace <= 0) {
    352                     break;
    353                 }
    354                 final String folderString = f.name;
    355                 final int fgColor = f.getForegroundColor(mDefaultFgColor);
    356                 final int bgColor = f.getBackgroundColor(mDefaultBgColor);
    357                 boolean labelTooLong = false;
    358                 final int textW = (int) sFoldersPaint.measureText(folderString);
    359                 int width = textW + cellSize + sFoldersLeftPadding;
    360 
    361                 if (overflow && width > averageWidth) {
    362                     if (i < foldersCount - 1) {
    363                         width = averageWidth;
    364                     } else {
    365                         // allow the last label to take all remaining space
    366                         // (and don't let it make room for padding)
    367                         width = availableSpace + sFoldersLeftPadding;
    368                     }
    369                     labelTooLong = true;
    370                 }
    371 
    372                 // TODO (mindyp): how to we get this?
    373                 final boolean isMuted = false;
    374                 // labelValues.folderId ==
    375                 // sGmail.getFolderMap(mAccount).getFolderIdIgnored();
    376 
    377                 // Draw the box.
    378                 sFoldersPaint.setColor(bgColor);
    379                 sFoldersPaint.setStyle(Paint.Style.FILL);
    380                 canvas.drawRect(xStart, y, xStart + width - sFoldersLeftPadding,
    381                         y + height, sFoldersPaint);
    382 
    383                 // Draw the text.
    384                 final int padding = cellSize / 2;
    385                 sFoldersPaint.setColor(fgColor);
    386                 sFoldersPaint.setStyle(Paint.Style.FILL);
    387                 if (labelTooLong) {
    388                     final int rightBorder = xStart + width - sFoldersLeftPadding - padding;
    389                     final Shader shader = new LinearGradient(rightBorder - padding, y, rightBorder,
    390                             y, fgColor, Utils.getTransparentColor(fgColor), Shader.TileMode.CLAMP);
    391                     sFoldersPaint.setShader(shader);
    392                 }
    393                 canvas.drawText(folderString, xStart + padding, y + height - textBottomPadding,
    394                         sFoldersPaint);
    395                 if (labelTooLong) {
    396                     sFoldersPaint.setShader(null);
    397                 }
    398 
    399                 availableSpace -= width;
    400                 xStart += width;
    401                 i++;
    402             }
    403         }
    404     }
    405 
    406     public ConversationItemView(Context context, String account) {
    407         super(context);
    408         Utils.traceBeginSection("CIVC constructor");
    409         setClickable(true);
    410         setLongClickable(true);
    411         mContext = context.getApplicationContext();
    412         final Resources res = mContext.getResources();
    413         mTabletDevice = Utils.useTabletUI(res);
    414         mIsExpansiveTablet =
    415                 mTabletDevice ? res.getBoolean(R.bool.use_expansive_tablet_ui) : false;
    416         mListCollapsible = res.getBoolean(R.bool.list_collapsible);
    417         mAccount = account;
    418 
    419         if (STAR_OFF == null) {
    420             // Initialize static bitmaps.
    421             STAR_OFF = BitmapFactory.decodeResource(res, R.drawable.ic_btn_star_off);
    422             STAR_ON = BitmapFactory.decodeResource(res, R.drawable.ic_btn_star_on);
    423             CHECK = BitmapFactory.decodeResource(res, R.drawable.ic_avatar_check);
    424             ATTACHMENT = BitmapFactory.decodeResource(res, R.drawable.ic_attachment_holo_light);
    425             ONLY_TO_ME = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_double);
    426             TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_single);
    427             IMPORTANT_ONLY_TO_ME = BitmapFactory.decodeResource(res,
    428                     R.drawable.ic_email_caret_double_important_unread);
    429             IMPORTANT_TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res,
    430                     R.drawable.ic_email_caret_single_important_unread);
    431             IMPORTANT_TO_OTHERS = BitmapFactory.decodeResource(res,
    432                     R.drawable.ic_email_caret_none_important_unread);
    433             STATE_REPLIED =
    434                     BitmapFactory.decodeResource(res, R.drawable.ic_badge_reply_holo_light);
    435             STATE_FORWARDED =
    436                     BitmapFactory.decodeResource(res, R.drawable.ic_badge_forward_holo_light);
    437             STATE_REPLIED_AND_FORWARDED =
    438                     BitmapFactory.decodeResource(res, R.drawable.ic_badge_reply_forward_holo_light);
    439             STATE_CALENDAR_INVITE =
    440                     BitmapFactory.decodeResource(res, R.drawable.ic_badge_invite_holo_light);
    441             VISIBLE_CONVERSATION_CARET = BitmapFactory.decodeResource(res, R.drawable.caret_grey);
    442             RIGHT_EDGE_TABLET = res.getDrawable(R.drawable.list_edge_tablet);
    443             PLACEHOLDER = res.getDrawable(drawable.ic_attachment_load);
    444             PROGRESS_BAR = res.getDrawable(drawable.progress_holo);
    445 
    446             // Initialize colors.
    447             sActivatedTextSpan = CharacterStyle.wrap(new ForegroundColorSpan(
    448                     res.getColor(R.color.senders_text_color_read)));
    449             sSendersTextColorRead = res.getColor(R.color.senders_text_color_read);
    450             sSendersTextColorUnread = res.getColor(R.color.senders_text_color_unread);
    451             sSubjectTextUnreadSpan = new TextAppearanceSpan(mContext,
    452                     R.style.SubjectAppearanceUnreadStyle);
    453             sSubjectTextReadSpan = new TextAppearanceSpan(mContext,
    454                     R.style.SubjectAppearanceReadStyle);
    455             sSnippetTextUnreadSpan =
    456                     new ForegroundColorSpan(res.getColor(R.color.snippet_text_color_unread));
    457             sSnippetTextReadSpan =
    458                     new ForegroundColorSpan(res.getColor(R.color.snippet_text_color_read));
    459             sDateTextColor = res.getColor(R.color.date_text_color);
    460             sStarTouchSlop = res.getDimensionPixelSize(R.dimen.star_touch_slop);
    461             sSenderImageTouchSlop = res.getDimensionPixelSize(R.dimen.sender_image_touch_slop);
    462             sShrinkAnimationDuration = res.getInteger(R.integer.shrink_animation_duration);
    463             sSlideAnimationDuration = res.getInteger(R.integer.slide_animation_duration);
    464             // Initialize static color.
    465             sSendersSplitToken = res.getString(R.string.senders_split_token);
    466             sElidedPaddingToken = res.getString(R.string.elided_padding_token);
    467             sScrollSlop = res.getInteger(R.integer.swipeScrollSlop);
    468             sFoldersLeftPadding = res.getDimensionPixelOffset(R.dimen.folders_left_padding);
    469             sContactPhotoManager = ContactPhotoManager.createContactPhotoManager(context);
    470             sOverflowCountMax = res.getInteger(integer.ap_overflow_max_count);
    471             sCabAnimationDuration =
    472                     res.getInteger(R.integer.conv_item_view_cab_anim_duration);
    473         }
    474 
    475         mPhotoFlipAnimator = new CabAnimator("photoFlipFraction", 0, 2,
    476                 sCabAnimationDuration) {
    477             @Override
    478             public void invalidateArea() {
    479                 final int left = mCoordinates.contactImagesX;
    480                 final int right = left + mContactImagesHolder.getWidth();
    481                 final int top = mCoordinates.contactImagesY;
    482                 final int bottom = top + mContactImagesHolder.getHeight();
    483                 invalidate(left, top, right, bottom);
    484             }
    485         };
    486 
    487         mSendersTextView = new TextView(mContext);
    488         mSendersTextView.setIncludeFontPadding(false);
    489 
    490         mSubjectTextView = new TextView(mContext);
    491         mSubjectTextView.setEllipsize(TextUtils.TruncateAt.END);
    492         mSubjectTextView.setIncludeFontPadding(false);
    493 
    494         mContactImagesHolder = new DividedImageCanvas(context, new InvalidateCallback() {
    495             @Override
    496             public void invalidate() {
    497                 if (mCoordinates == null) {
    498                     return;
    499                 }
    500                 ConversationItemView.this.invalidate(mCoordinates.contactImagesX,
    501                         mCoordinates.contactImagesY,
    502                         mCoordinates.contactImagesX + mCoordinates.contactImagesWidth,
    503                         mCoordinates.contactImagesY + mCoordinates.contactImagesHeight);
    504             }
    505         });
    506 
    507         mAttachmentsView = new AttachmentGridDrawable(res, PLACEHOLDER, PROGRESS_BAR);
    508         mAttachmentsView.setCallback(this);
    509 
    510         Utils.traceEndSection();
    511     }
    512 
    513     public void bind(final Conversation conversation, final ControllableActivity activity,
    514             final ConversationListListener conversationListListener,
    515             final ConversationSelectionSet set, final Folder folder,
    516             final int checkboxOrSenderImage, final boolean showAttachmentPreviews,
    517             final boolean parallaxSpeedAlternative, final boolean parallaxDirectionAlternative,
    518             final boolean swipeEnabled, final boolean priorityArrowEnabled,
    519             final AnimatedAdapter adapter) {
    520         Utils.traceBeginSection("CIVC.bind");
    521         bind(ConversationItemViewModel.forConversation(mAccount, conversation), activity,
    522                 conversationListListener, null /* conversationItemAreaClickListener */, set, folder,
    523                 checkboxOrSenderImage, showAttachmentPreviews, parallaxSpeedAlternative,
    524                 parallaxDirectionAlternative, swipeEnabled, priorityArrowEnabled, adapter,
    525                 -1 /* backgroundOverrideResId */,
    526                 null /* photoBitmap */);
    527         Utils.traceEndSection();
    528     }
    529 
    530     public void bindAd(final ConversationItemViewModel conversationItemViewModel,
    531             final ControllableActivity activity,
    532             final ConversationListListener conversationListListener,
    533             final ConversationItemAreaClickListener conversationItemAreaClickListener,
    534             final Folder folder, final int checkboxOrSenderImage, final AnimatedAdapter adapter,
    535             final int backgroundOverrideResId, final Bitmap photoBitmap) {
    536         Utils.traceBeginSection("CIVC.bindAd");
    537         bind(conversationItemViewModel, activity, conversationListListener,
    538                 conversationItemAreaClickListener, null /* set */, folder, checkboxOrSenderImage,
    539                 false /* attachment previews */, false /* parallax */, false /* parallax */,
    540                 true /* swipeEnabled */, false /* priorityArrowEnabled */, adapter,
    541                 backgroundOverrideResId, photoBitmap);
    542         Utils.traceEndSection();
    543     }
    544 
    545     private void bind(final ConversationItemViewModel header, final ControllableActivity activity,
    546             final ConversationListListener conversationListListener,
    547             final ConversationItemAreaClickListener conversationItemAreaClickListener,
    548             final ConversationSelectionSet set, final Folder folder,
    549             final int checkboxOrSenderImage, final boolean showAttachmentPreviews,
    550             final boolean parallaxSpeedAlternative, final boolean parallaxDirectionAlternative,
    551             boolean swipeEnabled, final boolean priorityArrowEnabled, final AnimatedAdapter adapter,
    552             final int backgroundOverrideResId, final Bitmap photoBitmap) {
    553         mBackgroundOverrideResId = backgroundOverrideResId;
    554         mPhotoBitmap = photoBitmap;
    555         mConversationItemAreaClickListener = conversationItemAreaClickListener;
    556 
    557         if (mHeader != null) {
    558             // If this was previously bound to a different conversation, remove any contact photo
    559             // manager requests.
    560             if (header.conversation.id != mHeader.conversation.id ||
    561                     (mHeader.displayableSenderNames != null && !mHeader.displayableSenderNames
    562                     .equals(header.displayableSenderNames))) {
    563                 ArrayList<String> divisionIds = mContactImagesHolder.getDivisionIds();
    564                 if (divisionIds != null) {
    565                     mContactImagesHolder.reset();
    566                     for (int pos = 0; pos < divisionIds.size(); pos++) {
    567                         sContactPhotoManager.removePhoto(ContactPhotoManager.generateHash(
    568                                 mContactImagesHolder, pos, divisionIds.get(pos)));
    569                     }
    570                 }
    571             }
    572 
    573             // If this was previously bound to a different conversation,
    574             // remove any attachment preview manager requests.
    575             if (header.conversation.id != mHeader.conversation.id
    576                     || header.conversation.attachmentPreviewsCount
    577                             != mHeader.conversation.attachmentPreviewsCount
    578                     || !header.conversation.getAttachmentPreviewUris()
    579                             .equals(mHeader.conversation.getAttachmentPreviewUris())) {
    580 
    581                 // unbind the attachments view (releasing bitmap references)
    582                 // (this also cancels all async tasks)
    583                 for (int i = 0, len = mAttachmentsView.getCount(); i < len; i++) {
    584                     mAttachmentsView.getOrCreateDrawable(i).unbind();
    585                 }
    586                 // reset the grid, as the newly bound item may have a different attachment count
    587                 mAttachmentsView.setCount(0);
    588             }
    589 
    590             if (header.conversation.id != mHeader.conversation.id) {
    591                 // Stop the photo flip animation
    592                 mPhotoFlipAnimator.stopAnimation();
    593             }
    594         }
    595         mCoordinates = null;
    596         mHeader = header;
    597         mActivity = activity;
    598         mConversationListListener = conversationListListener;
    599         mSelectedConversationSet = set;
    600         mDisplayedFolder = folder;
    601         mStarEnabled = folder != null && !folder.isTrash();
    602         mSwipeEnabled = swipeEnabled;
    603         mAdapter = adapter;
    604         mAttachmentsView.setBitmapCache(mAdapter.getBitmapCache());
    605         mAttachmentsView.setDecodeAggregator(mAdapter.getDecodeAggregator());
    606 
    607         if (checkboxOrSenderImage == ConversationListIcon.SENDER_IMAGE) {
    608             mGadgetMode = ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO;
    609         } else {
    610             mGadgetMode = ConversationItemViewCoordinates.GADGET_NONE;
    611         }
    612 
    613         mAttachmentPreviewsEnabled = showAttachmentPreviews;
    614         mParallaxSpeedAlternative = parallaxSpeedAlternative;
    615         mParallaxDirectionAlternative = parallaxDirectionAlternative;
    616 
    617         // Initialize folder displayer.
    618         if (mHeader.folderDisplayer == null) {
    619             mHeader.folderDisplayer = new ConversationItemFolderDisplayer(mContext);
    620         } else {
    621             mHeader.folderDisplayer.reset();
    622         }
    623 
    624         final int ignoreFolderType;
    625         if (mDisplayedFolder.isInbox()) {
    626             ignoreFolderType = FolderType.INBOX;
    627         } else {
    628             ignoreFolderType = -1;
    629         }
    630 
    631         mHeader.folderDisplayer.loadConversationFolders(mHeader.conversation,
    632                 mDisplayedFolder.folderUri, ignoreFolderType);
    633 
    634         if (mHeader.dateOverrideText == null) {
    635             mHeader.dateText = DateUtils.getRelativeTimeSpanString(mContext,
    636                     mHeader.conversation.dateMs);
    637         } else {
    638             mHeader.dateText = mHeader.dateOverrideText;
    639         }
    640 
    641         mConfig = new ConversationItemViewCoordinates.Config()
    642             .withGadget(mGadgetMode)
    643             .withAttachmentPreviews(getAttachmentPreviewsMode());
    644         if (header.folderDisplayer.hasVisibleFolders()) {
    645             mConfig.showFolders();
    646         }
    647         if (header.hasBeenForwarded || header.hasBeenRepliedTo || header.isInvite) {
    648             mConfig.showReplyState();
    649         }
    650         if (mHeader.conversation.color != 0) {
    651             mConfig.showColorBlock();
    652         }
    653         // Personal level.
    654         mHeader.personalLevelBitmap = null;
    655         if (true) { // TODO: hook this up to a setting
    656             final int personalLevel = mHeader.conversation.personalLevel;
    657             final boolean isImportant =
    658                     mHeader.conversation.priority == UIProvider.ConversationPriority.IMPORTANT;
    659             final boolean useImportantMarkers = isImportant && priorityArrowEnabled;
    660 
    661             if (personalLevel == UIProvider.ConversationPersonalLevel.ONLY_TO_ME) {
    662                 mHeader.personalLevelBitmap = useImportantMarkers ? IMPORTANT_ONLY_TO_ME
    663                         : ONLY_TO_ME;
    664             } else if (personalLevel == UIProvider.ConversationPersonalLevel.TO_ME_AND_OTHERS) {
    665                 mHeader.personalLevelBitmap = useImportantMarkers ? IMPORTANT_TO_ME_AND_OTHERS
    666                         : TO_ME_AND_OTHERS;
    667             } else if (useImportantMarkers) {
    668                 mHeader.personalLevelBitmap = IMPORTANT_TO_OTHERS;
    669             }
    670         }
    671         if (mHeader.personalLevelBitmap != null) {
    672             mConfig.showPersonalIndicator();
    673         }
    674 
    675         mAttachmentsView.setOverflowText(null);
    676 
    677         setContentDescription();
    678         requestLayout();
    679     }
    680 
    681     @Override
    682     public void invalidateDrawable(Drawable who) {
    683         boolean handled = false;
    684         if (mCoordinates != null) {
    685             if (mAttachmentsView.equals(who)) {
    686                 final Rect r = new Rect(who.getBounds());
    687                 r.offset(mCoordinates.attachmentPreviewsX, mCoordinates.attachmentPreviewsY);
    688                 ConversationItemView.this.invalidate(r.left, r.top, r.right, r.bottom);
    689                 handled = true;
    690             }
    691         }
    692         if (!handled) {
    693             super.invalidateDrawable(who);
    694         }
    695     }
    696 
    697     /**
    698      * Get the Conversation object associated with this view.
    699      */
    700     public Conversation getConversation() {
    701         return mHeader.conversation;
    702     }
    703 
    704     private static void startTimer(String tag) {
    705         if (sTimer != null) {
    706             sTimer.start(tag);
    707         }
    708     }
    709 
    710     private static void pauseTimer(String tag) {
    711         if (sTimer != null) {
    712             sTimer.pause(tag);
    713         }
    714     }
    715 
    716     @Override
    717     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    718         Utils.traceBeginSection("CIVC.measure");
    719         final int wSize = MeasureSpec.getSize(widthMeasureSpec);
    720 
    721         final int currentMode = mActivity.getViewMode().getMode();
    722         if (wSize != mViewWidth || mPreviousMode != currentMode) {
    723             mViewWidth = wSize;
    724             mPreviousMode = currentMode;
    725         }
    726         mHeader.viewWidth = mViewWidth;
    727 
    728         mConfig.updateWidth(wSize).setViewMode(currentMode);
    729 
    730         Resources res = getResources();
    731         mHeader.standardScaledDimen = res.getDimensionPixelOffset(R.dimen.standard_scaled_dimen);
    732 
    733         mCoordinates = ConversationItemViewCoordinates.forConfig(mContext, mConfig,
    734                 mAdapter.getCoordinatesCache());
    735 
    736         if (mPhotoBitmap != null) {
    737             mPhotoRect = new Rect(0, 0, mCoordinates.contactImagesWidth,
    738                     mCoordinates.contactImagesHeight);
    739         }
    740 
    741         final int h = (mAnimatedHeightFraction != 1.0f) ?
    742                 Math.round(mAnimatedHeightFraction * mCoordinates.height) : mCoordinates.height;
    743         setMeasuredDimension(mConfig.getWidth(), h);
    744         Utils.traceEndSection();
    745     }
    746 
    747     @Override
    748     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    749         startTimer(PERF_TAG_LAYOUT);
    750         Utils.traceBeginSection("CIVC.layout");
    751 
    752         super.onLayout(changed, left, top, right, bottom);
    753 
    754         Utils.traceBeginSection("text and bitmaps");
    755         calculateTextsAndBitmaps();
    756         Utils.traceEndSection();
    757 
    758         Utils.traceBeginSection("coordinates");
    759         calculateCoordinates();
    760         Utils.traceEndSection();
    761 
    762         // Subject.
    763         createSubject(mHeader.unread);
    764 
    765         if (!mHeader.isLayoutValid()) {
    766             setContentDescription();
    767         }
    768         mHeader.validate();
    769 
    770         pauseTimer(PERF_TAG_LAYOUT);
    771         if (sTimer != null && ++sLayoutCount >= PERF_LAYOUT_ITERATIONS) {
    772             sTimer.dumpResults();
    773             sTimer = new Timer();
    774             sLayoutCount = 0;
    775         }
    776         Utils.traceEndSection();
    777     }
    778 
    779     private void setContentDescription() {
    780         if (mActivity.isAccessibilityEnabled()) {
    781             mHeader.resetContentDescription();
    782             setContentDescription(mHeader.getContentDescription(mContext));
    783         }
    784     }
    785 
    786     @Override
    787     public void setBackgroundResource(int resourceId) {
    788         Utils.traceBeginSection("set background resource");
    789         Drawable drawable = mBackgrounds.get(resourceId);
    790         if (drawable == null) {
    791             drawable = getResources().getDrawable(resourceId);
    792             mBackgrounds.put(resourceId, drawable);
    793         }
    794         if (getBackground() != drawable) {
    795             super.setBackgroundDrawable(drawable);
    796         }
    797         Utils.traceEndSection();
    798     }
    799 
    800     private void calculateTextsAndBitmaps() {
    801         startTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
    802 
    803         if (mSelectedConversationSet != null) {
    804             mSelected = mSelectedConversationSet.contains(mHeader.conversation);
    805         }
    806         setSelected(mSelected);
    807         mHeader.gadgetMode = mGadgetMode;
    808 
    809         final boolean isUnread = mHeader.unread;
    810         updateBackground(isUnread);
    811 
    812         mHeader.sendersDisplayText = new SpannableStringBuilder();
    813         mHeader.styledSendersString = null;
    814 
    815         mHeader.hasDraftMessage = mHeader.conversation.numDrafts() > 0;
    816 
    817         // Parse senders fragments.
    818         if (mHeader.preserveSendersText) {
    819             // This is a special view that doesn't need special sender formatting
    820             mHeader.sendersDisplayText = new SpannableStringBuilder(mHeader.sendersText);
    821             loadSenderImages();
    822         } else if (mHeader.conversation.conversationInfo != null) {
    823             // This is Gmail
    824             Context context = getContext();
    825             mHeader.messageInfoString = SendersView
    826                     .createMessageInfo(context, mHeader.conversation, true);
    827             int maxChars = ConversationItemViewCoordinates.getSendersLength(context,
    828                     mCoordinates.getMode(), mHeader.conversation.hasAttachments);
    829             mHeader.displayableSenderEmails = new ArrayList<String>();
    830             mHeader.displayableSenderNames = new ArrayList<String>();
    831             mHeader.styledSenders = new ArrayList<SpannableString>();
    832             SendersView.format(context, mHeader.conversation.conversationInfo,
    833                     mHeader.messageInfoString.toString(), maxChars, mHeader.styledSenders,
    834                     mHeader.displayableSenderNames, mHeader.displayableSenderEmails, mAccount,
    835                     true);
    836 
    837             if (mHeader.displayableSenderEmails.isEmpty() && mHeader.hasDraftMessage) {
    838                 mHeader.displayableSenderEmails.add(mAccount);
    839                 mHeader.displayableSenderNames.add(mAccount);
    840             }
    841 
    842             // If we have displayable senders, load their thumbnails
    843             loadSenderImages();
    844         } else {
    845             // This is Email
    846             SendersView.formatSenders(mHeader, getContext(), true);
    847             if (!TextUtils.isEmpty(mHeader.conversation.senders)) {
    848                 mHeader.displayableSenderEmails = new ArrayList<String>();
    849                 mHeader.displayableSenderNames = new ArrayList<String>();
    850 
    851                 final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(mHeader.conversation.senders);
    852                 for (int i = 0; i < tokens.length;i++) {
    853                     final Rfc822Token token = tokens[i];
    854                     final String senderName = Address.decodeAddressName(token.getName());
    855                     final String senderAddress = token.getAddress();
    856                     mHeader.displayableSenderEmails.add(senderAddress);
    857                     mHeader.displayableSenderNames.add(
    858                             !TextUtils.isEmpty(senderName) ? senderName : senderAddress);
    859                 }
    860                 loadSenderImages();
    861             }
    862         }
    863 
    864         if (isAttachmentPreviewsEnabled()) {
    865             loadAttachmentPreviews();
    866         }
    867 
    868         if (mHeader.isLayoutValid()) {
    869             pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
    870             return;
    871         }
    872         startTimer(PERF_TAG_CALCULATE_FOLDERS);
    873 
    874 
    875         pauseTimer(PERF_TAG_CALCULATE_FOLDERS);
    876 
    877         // Paper clip icon.
    878         mHeader.paperclip = null;
    879         if (mHeader.conversation.hasAttachments) {
    880             mHeader.paperclip = ATTACHMENT;
    881         }
    882 
    883         startTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT);
    884 
    885         pauseTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT);
    886         pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
    887     }
    888 
    889     private boolean isAttachmentPreviewsEnabled() {
    890         return mAttachmentPreviewsEnabled && !mHeader.conversation.getAttachmentPreviewUris()
    891                 .isEmpty();
    892     }
    893 
    894     private int getOverflowCount() {
    895         return mHeader.conversation.attachmentPreviewsCount - mHeader.conversation
    896                 .getAttachmentPreviewUris().size();
    897     }
    898 
    899     private int getAttachmentPreviewsMode() {
    900         if (isAttachmentPreviewsEnabled()) {
    901             return mHeader.conversation.read
    902                     ? ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_READ
    903                     : ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_UNREAD;
    904         } else {
    905             return ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_NONE;
    906         }
    907     }
    908 
    909     private float getParallaxSpeedMultiplier() {
    910         return mParallaxSpeedAlternative
    911                 ? SwipeableListView.ATTACHMENT_PARALLAX_MULTIPLIER_ALTERNATIVE
    912                 : SwipeableListView.ATTACHMENT_PARALLAX_MULTIPLIER_NORMAL;
    913     }
    914 
    915     // FIXME(ath): maybe move this to bind(). the only dependency on layout is on tile W/H, which
    916     // is immutable.
    917     private void loadSenderImages() {
    918         if (mGadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO
    919                 && mHeader.displayableSenderEmails != null
    920                 && mHeader.displayableSenderEmails.size() > 0) {
    921             if (mCoordinates.contactImagesWidth <= 0 || mCoordinates.contactImagesHeight <= 0) {
    922                 LogUtils.w(LOG_TAG,
    923                         "Contact image width(%d) or height(%d) is 0 for mode: (%d).",
    924                         mCoordinates.contactImagesWidth, mCoordinates.contactImagesHeight,
    925                         mCoordinates.getMode());
    926                 return;
    927             }
    928 
    929             int size = mHeader.displayableSenderEmails.size();
    930             final List<Object> keys = Lists.newArrayListWithCapacity(size);
    931             for (int i = 0; i < DividedImageCanvas.MAX_DIVISIONS && i < size; i++) {
    932                 keys.add(mHeader.displayableSenderEmails.get(i));
    933             }
    934 
    935             mContactImagesHolder.setDimensions(mCoordinates.contactImagesWidth,
    936                     mCoordinates.contactImagesHeight);
    937             mContactImagesHolder.setDivisionIds(keys);
    938             String emailAddress;
    939             for (int i = 0; i < DividedImageCanvas.MAX_DIVISIONS && i < size; i++) {
    940                 emailAddress = mHeader.displayableSenderEmails.get(i);
    941                 PhotoIdentifier photoIdentifier = new ContactIdentifier(
    942                         mHeader.displayableSenderNames.get(i), emailAddress, i);
    943                 sContactPhotoManager.loadThumbnail(photoIdentifier, mContactImagesHolder);
    944             }
    945         }
    946     }
    947 
    948     private void loadAttachmentPreviews() {
    949         if (mCoordinates.attachmentPreviewsWidth <= 0
    950                 || mCoordinates.attachmentPreviewsHeight <= 0) {
    951             LogUtils.w(LOG_TAG,
    952                     "Attachment preview width(%d) or height(%d) is 0 for mode: (%d,%d).",
    953                     mCoordinates.attachmentPreviewsWidth, mCoordinates.attachmentPreviewsHeight,
    954                     mCoordinates.getMode(), getAttachmentPreviewsMode());
    955             return;
    956         }
    957         Utils.traceBeginSection("attachment previews");
    958 
    959         Utils.traceBeginSection("Setup load attachment previews");
    960 
    961         LogUtils.d(LOG_TAG,
    962                 "loadAttachmentPreviews: Loading attachment previews for conversation %s",
    963                 mHeader.conversation);
    964 
    965         // Get list of attachments and states from conversation
    966         final ArrayList<String> attachmentUris = mHeader.conversation.getAttachmentPreviewUris();
    967         final int previewStates = mHeader.conversation.attachmentPreviewStates;
    968         final int displayCount = Math.min(
    969                 attachmentUris.size(), AttachmentGridDrawable.MAX_VISIBLE_ATTACHMENT_COUNT);
    970         Utils.traceEndSection();
    971 
    972         mAttachmentsView.setCoordinates(mCoordinates);
    973         mAttachmentsView.setCount(displayCount);
    974 
    975         final int decodeHeight;
    976         // if parallax is enabled, increase the desired vertical size of attachment bitmaps
    977         // so we have extra pixels to scroll within
    978         if (SwipeableListView.ENABLE_ATTACHMENT_PARALLAX) {
    979             decodeHeight = Math.round(mCoordinates.attachmentPreviewsDecodeHeight
    980                     * getParallaxSpeedMultiplier());
    981         } else {
    982             decodeHeight = mCoordinates.attachmentPreviewsDecodeHeight;
    983         }
    984 
    985         // set the bounds before binding inner drawables so they can decode right away
    986         // (they need the their bounds set to know whether to decode to 1x1 or 2x1 dimens)
    987         mAttachmentsView.setBounds(0, 0, mCoordinates.attachmentPreviewsWidth,
    988                 mCoordinates.attachmentPreviewsHeight);
    989 
    990         for (int i = 0; i < displayCount; i++) {
    991             Utils.traceBeginSection("setup single attachment preview");
    992             final String uri = attachmentUris.get(i);
    993 
    994             // Find the rendition to load based on availability.
    995             LogUtils.v(LOG_TAG, "loadAttachmentPreviews: state [BEST, SIMPLE] is [%s, %s] for %s ",
    996                     Attachment.getPreviewState(previewStates, i, AttachmentRendition.BEST),
    997                     Attachment.getPreviewState(previewStates, i, AttachmentRendition.SIMPLE),
    998                     uri);
    999             int bestAvailableRendition = -1;
   1000             // BEST first, else use less preferred renditions
   1001             for (final int rendition : AttachmentRendition.PREFERRED_RENDITIONS) {
   1002                 if (Attachment.getPreviewState(previewStates, i, rendition)) {
   1003                     bestAvailableRendition = rendition;
   1004                     break;
   1005                 }
   1006             }
   1007 
   1008             LogUtils.d(LOG_TAG,
   1009                     "creating/setting drawable region in CIV=%s canvas=%s rend=%s uri=%s",
   1010                     this, mAttachmentsView, bestAvailableRendition, uri);
   1011             final AttachmentDrawable drawable = mAttachmentsView.getOrCreateDrawable(i);
   1012             drawable.setDecodeDimensions(mCoordinates.attachmentPreviewsWidth, decodeHeight);
   1013             drawable.setParallaxSpeedMultiplier(getParallaxSpeedMultiplier());
   1014             if (bestAvailableRendition != -1) {
   1015                 drawable.bind(getContext(), uri, bestAvailableRendition);
   1016             } else {
   1017                 drawable.showStaticPlaceholder();
   1018             }
   1019 
   1020             Utils.traceEndSection();
   1021         }
   1022 
   1023         Utils.traceEndSection();
   1024     }
   1025 
   1026     private static int makeExactSpecForSize(int size) {
   1027         return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
   1028     }
   1029 
   1030     private static void layoutViewExactly(View v, int w, int h) {
   1031         v.measure(makeExactSpecForSize(w), makeExactSpecForSize(h));
   1032         v.layout(0, 0, w, h);
   1033     }
   1034 
   1035     private void layoutSenders() {
   1036         if (mHeader.styledSendersString != null) {
   1037             if (isActivated() && showActivatedText()) {
   1038                 mHeader.styledSendersString.setSpan(sActivatedTextSpan, 0,
   1039                         mHeader.styledMessageInfoStringOffset, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
   1040             } else {
   1041                 mHeader.styledSendersString.removeSpan(sActivatedTextSpan);
   1042             }
   1043 
   1044             final int w = mSendersWidth;
   1045             final int h = mCoordinates.sendersHeight;
   1046             mSendersTextView.setLayoutParams(new ViewGroup.LayoutParams(w, h));
   1047             mSendersTextView.setMaxLines(mCoordinates.sendersLineCount);
   1048             mSendersTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCoordinates.sendersFontSize);
   1049             layoutViewExactly(mSendersTextView, w, h);
   1050 
   1051             mSendersTextView.setText(mHeader.styledSendersString);
   1052         }
   1053     }
   1054 
   1055     private void createSubject(final boolean isUnread) {
   1056         final String subject = filterTag(mHeader.conversation.subject);
   1057         final String snippet = mHeader.conversation.getSnippet();
   1058         final Spannable displayedStringBuilder = new SpannableString(
   1059                 Conversation.getSubjectAndSnippetForDisplay(mContext, subject, snippet));
   1060 
   1061         // since spans affect text metrics, add spans to the string before measure/layout or fancy
   1062         // ellipsizing
   1063         final int subjectTextLength = (subject != null) ? subject.length() : 0;
   1064         if (!TextUtils.isEmpty(subject)) {
   1065             displayedStringBuilder.setSpan(TextAppearanceSpan.wrap(
   1066                     isUnread ? sSubjectTextUnreadSpan : sSubjectTextReadSpan), 0, subjectTextLength,
   1067                     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
   1068         }
   1069         if (!TextUtils.isEmpty(snippet)) {
   1070             final int startOffset = subjectTextLength;
   1071             // Start after the end of the subject text; since the subject may be
   1072             // "" or null, this could start at the 0th character in the subjectText string
   1073             displayedStringBuilder.setSpan(ForegroundColorSpan.wrap(
   1074                     isUnread ? sSnippetTextUnreadSpan : sSnippetTextReadSpan), startOffset,
   1075                     displayedStringBuilder.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
   1076         }
   1077         if (isActivated() && showActivatedText()) {
   1078             displayedStringBuilder.setSpan(sActivatedTextSpan, 0, displayedStringBuilder.length(),
   1079                     Spannable.SPAN_INCLUSIVE_INCLUSIVE);
   1080         }
   1081 
   1082         final int subjectWidth = mCoordinates.subjectWidth;
   1083         final int subjectHeight = mCoordinates.subjectHeight;
   1084         mSubjectTextView.setLayoutParams(new ViewGroup.LayoutParams(subjectWidth, subjectHeight));
   1085         mSubjectTextView.setMaxLines(mCoordinates.subjectLineCount);
   1086         mSubjectTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCoordinates.subjectFontSize);
   1087         layoutViewExactly(mSubjectTextView, subjectWidth, subjectHeight);
   1088 
   1089         mSubjectTextView.setText(displayedStringBuilder);
   1090     }
   1091 
   1092     private boolean showActivatedText() {
   1093         // For activated elements in tablet in conversation mode, we show an activated color, since
   1094         // the background is dark blue for activated versus gray for non-activated.
   1095         return mTabletDevice && !mListCollapsible;
   1096     }
   1097 
   1098     private boolean canFitFragment(int width, int line, int fixedWidth) {
   1099         if (line == mCoordinates.sendersLineCount) {
   1100             return width + fixedWidth <= mSendersWidth;
   1101         } else {
   1102             return width <= mSendersWidth;
   1103         }
   1104     }
   1105 
   1106     private void calculateCoordinates() {
   1107         startTimer(PERF_TAG_CALCULATE_COORDINATES);
   1108 
   1109         sPaint.setTextSize(mCoordinates.dateFontSize);
   1110         sPaint.setTypeface(Typeface.DEFAULT);
   1111 
   1112         if (mHeader.infoIcon != null) {
   1113             mInfoIconX = mCoordinates.infoIconXEnd - mHeader.infoIcon.getWidth();
   1114 
   1115             // If we have an info icon, we start drawing the date text:
   1116             // At the end of the date TextView minus the width of the date text
   1117             mDateX = mCoordinates.dateXEnd - (int) sPaint.measureText(
   1118                     mHeader.dateText != null ? mHeader.dateText.toString() : "");
   1119         } else {
   1120             // If there is no info icon, we start drawing the date text:
   1121             // At the end of the info icon ImageView minus the width of the date text
   1122             // We use the info icon ImageView for positioning, since we want the date text to be
   1123             // at the right, since there is no info icon
   1124             mDateX = mCoordinates.infoIconXEnd - (int) sPaint.measureText(
   1125                     mHeader.dateText != null ? mHeader.dateText.toString() : "");
   1126         }
   1127 
   1128         mPaperclipX = mDateX - ATTACHMENT.getWidth() - mCoordinates.datePaddingLeft;
   1129 
   1130         if (mCoordinates.isWide()) {
   1131             // In wide mode, the end of the senders should align with
   1132             // the start of the subject and is based on a max width.
   1133             mSendersWidth = mCoordinates.sendersWidth;
   1134         } else {
   1135             // In normal mode, the width is based on where the date/attachment icon start.
   1136             final int dateAttachmentStart;
   1137             // Have this end near the paperclip or date, not the folders.
   1138             if (mHeader.paperclip != null) {
   1139                 dateAttachmentStart = mPaperclipX - mCoordinates.paperclipPaddingLeft;
   1140             } else {
   1141                 dateAttachmentStart = mDateX - mCoordinates.datePaddingLeft;
   1142             }
   1143             mSendersWidth = dateAttachmentStart - mCoordinates.sendersX;
   1144         }
   1145 
   1146         // Second pass to layout each fragment.
   1147         sPaint.setTextSize(mCoordinates.sendersFontSize);
   1148         sPaint.setTypeface(Typeface.DEFAULT);
   1149 
   1150         if (mHeader.styledSenders != null) {
   1151             ellipsizeStyledSenders();
   1152             layoutSenders();
   1153         } else {
   1154             // First pass to calculate width of each fragment.
   1155             int totalWidth = 0;
   1156             int fixedWidth = 0;
   1157             for (SenderFragment senderFragment : mHeader.senderFragments) {
   1158                 CharacterStyle style = senderFragment.style;
   1159                 int start = senderFragment.start;
   1160                 int end = senderFragment.end;
   1161                 style.updateDrawState(sPaint);
   1162                 senderFragment.width = (int) sPaint.measureText(mHeader.sendersText, start, end);
   1163                 boolean isFixed = senderFragment.isFixed;
   1164                 if (isFixed) {
   1165                     fixedWidth += senderFragment.width;
   1166                 }
   1167                 totalWidth += senderFragment.width;
   1168             }
   1169 
   1170             if (mSendersWidth < 0) {
   1171                 mSendersWidth = 0;
   1172             }
   1173             totalWidth = ellipsize(fixedWidth);
   1174             mHeader.sendersDisplayLayout = new StaticLayout(mHeader.sendersDisplayText, sPaint,
   1175                     mSendersWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
   1176         }
   1177 
   1178         if (mSendersWidth < 0) {
   1179             mSendersWidth = 0;
   1180         }
   1181 
   1182         pauseTimer(PERF_TAG_CALCULATE_COORDINATES);
   1183     }
   1184 
   1185     // The rules for displaying ellipsized senders are as follows:
   1186     // 1) If there is message info (either a COUNT or DRAFT info to display), it MUST be shown
   1187     // 2) If senders do not fit, ellipsize the last one that does fit, and stop
   1188     // appending new senders
   1189     private int ellipsizeStyledSenders() {
   1190         SpannableStringBuilder builder = new SpannableStringBuilder();
   1191         float totalWidth = 0;
   1192         boolean ellipsize = false;
   1193         float width;
   1194         SpannableStringBuilder messageInfoString =  mHeader.messageInfoString;
   1195         if (messageInfoString.length() > 0) {
   1196             CharacterStyle[] spans = messageInfoString.getSpans(0, messageInfoString.length(),
   1197                     CharacterStyle.class);
   1198             // There is only 1 character style span; make sure we apply all the
   1199             // styles to the paint object before measuring.
   1200             if (spans.length > 0) {
   1201                 spans[0].updateDrawState(sPaint);
   1202             }
   1203             // Paint the message info string to see if we lose space.
   1204             float messageInfoWidth = sPaint.measureText(messageInfoString.toString());
   1205             totalWidth += messageInfoWidth;
   1206         }
   1207        SpannableString prevSender = null;
   1208        SpannableString ellipsizedText;
   1209         for (SpannableString sender : mHeader.styledSenders) {
   1210             // There may be null sender strings if there were dupes we had to remove.
   1211             if (sender == null) {
   1212                 continue;
   1213             }
   1214             // No more width available, we'll only show fixed fragments.
   1215             if (ellipsize) {
   1216                 break;
   1217             }
   1218             CharacterStyle[] spans = sender.getSpans(0, sender.length(), CharacterStyle.class);
   1219             // There is only 1 character style span.
   1220             if (spans.length > 0) {
   1221                 spans[0].updateDrawState(sPaint);
   1222             }
   1223             // If there are already senders present in this string, we need to
   1224             // make sure we prepend the dividing token
   1225             if (SendersView.sElidedString.equals(sender.toString())) {
   1226                 prevSender = sender;
   1227                 sender = copyStyles(spans, sElidedPaddingToken + sender + sElidedPaddingToken);
   1228             } else if (builder.length() > 0
   1229                     && (prevSender == null || !SendersView.sElidedString.equals(prevSender
   1230                             .toString()))) {
   1231                 prevSender = sender;
   1232                 sender = copyStyles(spans, sSendersSplitToken + sender);
   1233             } else {
   1234                 prevSender = sender;
   1235             }
   1236             if (spans.length > 0) {
   1237                 spans[0].updateDrawState(sPaint);
   1238             }
   1239             // Measure the width of the current sender and make sure we have space
   1240             width = (int) sPaint.measureText(sender.toString());
   1241             if (width + totalWidth > mSendersWidth) {
   1242                 // The text is too long, new line won't help. We have to
   1243                 // ellipsize text.
   1244                 ellipsize = true;
   1245                 width = mSendersWidth - totalWidth; // ellipsis width?
   1246                 ellipsizedText = copyStyles(spans,
   1247                         TextUtils.ellipsize(sender, sPaint, width, TruncateAt.END));
   1248                 width = (int) sPaint.measureText(ellipsizedText.toString());
   1249             } else {
   1250                 ellipsizedText = null;
   1251             }
   1252             totalWidth += width;
   1253 
   1254             final CharSequence fragmentDisplayText;
   1255             if (ellipsizedText != null) {
   1256                 fragmentDisplayText = ellipsizedText;
   1257             } else {
   1258                 fragmentDisplayText = sender;
   1259             }
   1260             builder.append(fragmentDisplayText);
   1261         }
   1262         mHeader.styledMessageInfoStringOffset = builder.length();
   1263         builder.append(messageInfoString);
   1264         mHeader.styledSendersString = builder;
   1265         return (int)totalWidth;
   1266     }
   1267 
   1268     private static SpannableString copyStyles(CharacterStyle[] spans, CharSequence newText) {
   1269         SpannableString s = new SpannableString(newText);
   1270         if (spans != null && spans.length > 0) {
   1271             s.setSpan(spans[0], 0, s.length(), 0);
   1272         }
   1273         return s;
   1274     }
   1275 
   1276     private int ellipsize(int fixedWidth) {
   1277         int totalWidth = 0;
   1278         int currentLine = 1;
   1279         boolean ellipsize = false;
   1280         for (SenderFragment senderFragment : mHeader.senderFragments) {
   1281             CharacterStyle style = senderFragment.style;
   1282             int start = senderFragment.start;
   1283             int end = senderFragment.end;
   1284             int width = senderFragment.width;
   1285             boolean isFixed = senderFragment.isFixed;
   1286             style.updateDrawState(sPaint);
   1287 
   1288             // No more width available, we'll only show fixed fragments.
   1289             if (ellipsize && !isFixed) {
   1290                 senderFragment.shouldDisplay = false;
   1291                 continue;
   1292             }
   1293 
   1294             // New line and ellipsize text if needed.
   1295             senderFragment.ellipsizedText = null;
   1296             if (isFixed) {
   1297                 fixedWidth -= width;
   1298             }
   1299             if (!canFitFragment(totalWidth + width, currentLine, fixedWidth)) {
   1300                 // The text is too long, new line won't help. We have to
   1301                 // ellipsize text.
   1302                 if (totalWidth == 0) {
   1303                     ellipsize = true;
   1304                 } else {
   1305                     // New line.
   1306                     if (currentLine < mCoordinates.sendersLineCount) {
   1307                         currentLine++;
   1308                         totalWidth = 0;
   1309                         // The text is still too long, we have to ellipsize
   1310                         // text.
   1311                         if (totalWidth + width > mSendersWidth) {
   1312                             ellipsize = true;
   1313                         }
   1314                     } else {
   1315                         ellipsize = true;
   1316                     }
   1317                 }
   1318 
   1319                 if (ellipsize) {
   1320                     width = mSendersWidth - totalWidth;
   1321                     // No more new line, we have to reserve width for fixed
   1322                     // fragments.
   1323                     if (currentLine == mCoordinates.sendersLineCount) {
   1324                         width -= fixedWidth;
   1325                     }
   1326                     senderFragment.ellipsizedText = TextUtils.ellipsize(
   1327                             mHeader.sendersText.substring(start, end), sPaint, width,
   1328                             TruncateAt.END).toString();
   1329                     width = (int) sPaint.measureText(senderFragment.ellipsizedText);
   1330                 }
   1331             }
   1332             senderFragment.shouldDisplay = true;
   1333             totalWidth += width;
   1334 
   1335             final CharSequence fragmentDisplayText;
   1336             if (senderFragment.ellipsizedText != null) {
   1337                 fragmentDisplayText = senderFragment.ellipsizedText;
   1338             } else {
   1339                 fragmentDisplayText = mHeader.sendersText.substring(start, end);
   1340             }
   1341             final int spanStart = mHeader.sendersDisplayText.length();
   1342             mHeader.sendersDisplayText.append(fragmentDisplayText);
   1343             mHeader.sendersDisplayText.setSpan(senderFragment.style, spanStart,
   1344                     mHeader.sendersDisplayText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
   1345         }
   1346         return totalWidth;
   1347     }
   1348 
   1349     /**
   1350      * If the subject contains the tag of a mailing-list (text surrounded with
   1351      * []), return the subject with that tag ellipsized, e.g.
   1352      * "[android-gmail-team] Hello" -> "[andr...] Hello"
   1353      */
   1354     private String filterTag(String subject) {
   1355         String result = subject;
   1356         String formatString = getContext().getResources().getString(R.string.filtered_tag);
   1357         if (!TextUtils.isEmpty(subject) && subject.charAt(0) == '[') {
   1358             int end = subject.indexOf(']');
   1359             if (end > 0) {
   1360                 String tag = subject.substring(1, end);
   1361                 result = String.format(formatString, Utils.ellipsize(tag, 7),
   1362                         subject.substring(end + 1));
   1363             }
   1364         }
   1365         return result;
   1366     }
   1367 
   1368     @Override
   1369     public final void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
   1370             int totalItemCount) {
   1371         if (SwipeableListView.ENABLE_ATTACHMENT_PARALLAX) {
   1372             if (mHeader == null || mCoordinates == null || !isAttachmentPreviewsEnabled()) {
   1373                 return;
   1374             }
   1375 
   1376             invalidate(mCoordinates.attachmentPreviewsX, mCoordinates.attachmentPreviewsY,
   1377                     mCoordinates.attachmentPreviewsX + mCoordinates.attachmentPreviewsWidth,
   1378                     mCoordinates.attachmentPreviewsY + mCoordinates.attachmentPreviewsHeight);
   1379         }
   1380     }
   1381 
   1382     @Override
   1383     public void onScrollStateChanged(AbsListView view, int scrollState) {
   1384     }
   1385 
   1386     @Override
   1387     protected void onDraw(Canvas canvas) {
   1388         Utils.traceBeginSection("CIVC.draw");
   1389 
   1390         // Contact photo
   1391         if (mGadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO) {
   1392             canvas.save();
   1393             drawContactImageArea(canvas);
   1394             canvas.restore();
   1395         }
   1396 
   1397         // Senders.
   1398         boolean isUnread = mHeader.unread;
   1399         // Old style senders; apply text colors/ sizes/ styling.
   1400         canvas.save();
   1401         if (mHeader.sendersDisplayLayout != null) {
   1402             sPaint.setTextSize(mCoordinates.sendersFontSize);
   1403             sPaint.setTypeface(SendersView.getTypeface(isUnread));
   1404             sPaint.setColor(isUnread ? sSendersTextColorUnread : sSendersTextColorRead);
   1405             canvas.translate(mCoordinates.sendersX, mCoordinates.sendersY
   1406                     + mHeader.sendersDisplayLayout.getTopPadding());
   1407             mHeader.sendersDisplayLayout.draw(canvas);
   1408         } else {
   1409             drawSenders(canvas);
   1410         }
   1411         canvas.restore();
   1412 
   1413 
   1414         // Subject.
   1415         sPaint.setTypeface(Typeface.DEFAULT);
   1416         canvas.save();
   1417         drawSubject(canvas);
   1418         canvas.restore();
   1419 
   1420         // Folders.
   1421         if (mConfig.areFoldersVisible()) {
   1422             mHeader.folderDisplayer.drawFolders(canvas, mCoordinates);
   1423         }
   1424 
   1425         // If this folder has a color (combined view/Email), show it here
   1426         if (mConfig.isColorBlockVisible()) {
   1427             sFoldersPaint.setColor(mHeader.conversation.color);
   1428             sFoldersPaint.setStyle(Paint.Style.FILL);
   1429             canvas.drawRect(mCoordinates.colorBlockX, mCoordinates.colorBlockY,
   1430                     mCoordinates.colorBlockX + mCoordinates.colorBlockWidth,
   1431                     mCoordinates.colorBlockY + mCoordinates.colorBlockHeight, sFoldersPaint);
   1432         }
   1433 
   1434         // Draw the reply state. Draw nothing if neither replied nor forwarded.
   1435         if (mConfig.isReplyStateVisible()) {
   1436             if (mHeader.hasBeenRepliedTo && mHeader.hasBeenForwarded) {
   1437                 canvas.drawBitmap(STATE_REPLIED_AND_FORWARDED, mCoordinates.replyStateX,
   1438                         mCoordinates.replyStateY, null);
   1439             } else if (mHeader.hasBeenRepliedTo) {
   1440                 canvas.drawBitmap(STATE_REPLIED, mCoordinates.replyStateX,
   1441                         mCoordinates.replyStateY, null);
   1442             } else if (mHeader.hasBeenForwarded) {
   1443                 canvas.drawBitmap(STATE_FORWARDED, mCoordinates.replyStateX,
   1444                         mCoordinates.replyStateY, null);
   1445             } else if (mHeader.isInvite) {
   1446                 canvas.drawBitmap(STATE_CALENDAR_INVITE, mCoordinates.replyStateX,
   1447                         mCoordinates.replyStateY, null);
   1448             }
   1449         }
   1450 
   1451         if (mConfig.isPersonalIndicatorVisible()) {
   1452             canvas.drawBitmap(mHeader.personalLevelBitmap, mCoordinates.personalIndicatorX,
   1453                     mCoordinates.personalIndicatorY, null);
   1454         }
   1455 
   1456         // Info icon
   1457         if (mHeader.infoIcon != null) {
   1458             canvas.drawBitmap(mHeader.infoIcon, mInfoIconX, mCoordinates.infoIconY, sPaint);
   1459         }
   1460 
   1461         // Date.
   1462         sPaint.setTextSize(mCoordinates.dateFontSize);
   1463         sPaint.setTypeface(Typeface.DEFAULT);
   1464         sPaint.setColor(sDateTextColor);
   1465         drawText(canvas, mHeader.dateText, mDateX, mCoordinates.dateYBaseline,
   1466                 sPaint);
   1467 
   1468         // Paper clip icon.
   1469         if (mHeader.paperclip != null) {
   1470             canvas.drawBitmap(mHeader.paperclip, mPaperclipX, mCoordinates.paperclipY, sPaint);
   1471         }
   1472 
   1473         if (mStarEnabled) {
   1474             // Star.
   1475             canvas.drawBitmap(getStarBitmap(), mCoordinates.starX, mCoordinates.starY, sPaint);
   1476         }
   1477 
   1478         // Attachment previews
   1479         if (isAttachmentPreviewsEnabled()) {
   1480             canvas.save();
   1481             drawAttachmentPreviews(canvas);
   1482             canvas.restore();
   1483         }
   1484 
   1485         // right-side edge effect when in tablet conversation mode and the list is not collapsed
   1486         if (Utils.getDisplayListRightEdgeEffect(mTabletDevice, mListCollapsible,
   1487                 mConfig.getViewMode())) {
   1488             RIGHT_EDGE_TABLET.setBounds(getWidth() - RIGHT_EDGE_TABLET.getIntrinsicWidth(), 0,
   1489                     getWidth(), getHeight());
   1490             RIGHT_EDGE_TABLET.draw(canvas);
   1491 
   1492             if (isActivated()) {
   1493                 // draw caret on the right, centered vertically
   1494                 final int x = getWidth() - VISIBLE_CONVERSATION_CARET.getWidth();
   1495                 final int y = (getHeight() - VISIBLE_CONVERSATION_CARET.getHeight()) / 2;
   1496                 canvas.drawBitmap(VISIBLE_CONVERSATION_CARET, x, y, null);
   1497             }
   1498         }
   1499         Utils.traceEndSection();
   1500     }
   1501 
   1502     /**
   1503      * Draws the contact images or check, in the correct animated state.
   1504      */
   1505     private void drawContactImageArea(final Canvas canvas) {
   1506         if (isSelected()) {
   1507             mLastSelectedId = mHeader.conversation.id;
   1508 
   1509             // Since this is selected, we draw the checkbox if the animation is not running, or if
   1510             // it's running, and is past the half-way point
   1511             if (mPhotoFlipAnimator.getValue() > 1 || !mPhotoFlipAnimator.isStarted()) {
   1512                 // Flash in the check
   1513                 drawCheckbox(canvas);
   1514             } else {
   1515                 // Flip out the contact photo
   1516                 drawContactImages(canvas);
   1517             }
   1518         } else {
   1519             if ((mConversationListListener.isExitingSelectionMode()
   1520                     && mLastSelectedId == mHeader.conversation.id)
   1521                     || mPhotoFlipAnimator.isStarted()) {
   1522                 // Animate back to the photo
   1523                 if (!mPhotoFlipAnimator.isStarted()) {
   1524                     mPhotoFlipAnimator.startAnimation(true /* reverse */);
   1525                 }
   1526 
   1527                 if (mPhotoFlipAnimator.getValue() > 1) {
   1528                     // Flash out the check
   1529                     drawCheckbox(canvas);
   1530                 } else {
   1531                     // Flip in the contact photo
   1532                     drawContactImages(canvas);
   1533                 }
   1534             } else {
   1535                 mLastSelectedId = -1; // We don't care anymore
   1536                 mPhotoFlipAnimator.stopAnimation(); // It's not running, but we want to reset state
   1537 
   1538                 // Contact photos
   1539                 drawContactImages(canvas);
   1540             }
   1541         }
   1542     }
   1543 
   1544     private void drawContactImages(final Canvas canvas) {
   1545         // mPhotoFlipFraction goes from 0 to 1
   1546         final float value = mPhotoFlipAnimator.getValue();
   1547 
   1548         final float scale = 1f - value;
   1549         final float xOffset = mContactImagesHolder.getWidth() * value / 2;
   1550 
   1551         mPhotoFlipMatrix.reset();
   1552         mPhotoFlipMatrix.postScale(scale, 1);
   1553 
   1554         final float x = mCoordinates.contactImagesX + xOffset;
   1555         final float y = mCoordinates.contactImagesY;
   1556 
   1557         canvas.translate(x, y);
   1558 
   1559         if (mPhotoBitmap == null) {
   1560             mContactImagesHolder.draw(canvas, mPhotoFlipMatrix);
   1561         } else {
   1562             canvas.drawBitmap(mPhotoBitmap, null, mPhotoRect, sPaint);
   1563         }
   1564     }
   1565 
   1566     private void drawCheckbox(final Canvas canvas) {
   1567         // mPhotoFlipFraction goes from 1 to 2
   1568 
   1569         // Draw the background
   1570         canvas.save();
   1571         canvas.translate(mCoordinates.contactImagesX, mCoordinates.contactImagesY);
   1572         canvas.drawRect(0, 0, mCoordinates.contactImagesWidth, mCoordinates.contactImagesHeight,
   1573                 sCheckBackgroundPaint);
   1574         canvas.restore();
   1575 
   1576         final int x = mCoordinates.contactImagesX
   1577                 + (mCoordinates.contactImagesWidth - CHECK.getWidth()) / 2;
   1578         final int y = mCoordinates.contactImagesY
   1579                 + (mCoordinates.contactImagesHeight - CHECK.getHeight()) / 2;
   1580 
   1581         final float value = mPhotoFlipAnimator.getValue();
   1582         final float scale;
   1583 
   1584         if (!mPhotoFlipAnimator.isStarted()) {
   1585             // We're not animating
   1586             scale = 1;
   1587         } else if (value < 1.9) {
   1588             // 1.0 to 1.9 will scale 0 to 1
   1589             scale = (value - 1f) / 0.9f;
   1590         } else if (value < 1.95) {
   1591             // 1.9 to 1.95 will scale 1 to 19/18
   1592             scale = (value - 1f) / 0.9f;
   1593         } else {
   1594             // 1.95 to 2.0 will scale 19/18 to 1
   1595             scale = (0.95f - (value - 1.95f)) / 0.9f;
   1596         }
   1597 
   1598         final float xOffset = CHECK.getWidth() * (1f - scale) / 2f;
   1599         final float yOffset = CHECK.getHeight() * (1f - scale) / 2f;
   1600 
   1601         mCheckMatrix.reset();
   1602         mCheckMatrix.postScale(scale, scale);
   1603 
   1604         canvas.translate(x + xOffset, y + yOffset);
   1605 
   1606         canvas.drawBitmap(CHECK, mCheckMatrix, sPaint);
   1607     }
   1608 
   1609     private void drawAttachmentPreviews(Canvas canvas) {
   1610         canvas.translate(mCoordinates.attachmentPreviewsX, mCoordinates.attachmentPreviewsY);
   1611         final float fraction;
   1612         if (SwipeableListView.ENABLE_ATTACHMENT_PARALLAX) {
   1613             final View listView = getListView();
   1614             final View listItemView = unwrap();
   1615             if (mParallaxDirectionAlternative) {
   1616                 fraction = 1 - (float) listItemView.getBottom()
   1617                         / (listView.getHeight() + listItemView.getHeight());
   1618             } else {
   1619                 fraction = (float) listItemView.getBottom()
   1620                         / (listView.getHeight() + listItemView.getHeight());
   1621             }
   1622         } else {
   1623             // Vertically center the preview crop, which has already been decoded at 1/3.
   1624             fraction = 0.5f;
   1625         }
   1626         mAttachmentsView.setParallaxFraction(fraction);
   1627         mAttachmentsView.draw(canvas);
   1628     }
   1629 
   1630     private void drawSubject(Canvas canvas) {
   1631         canvas.translate(mCoordinates.subjectX, mCoordinates.subjectY);
   1632         mSubjectTextView.draw(canvas);
   1633     }
   1634 
   1635     private void drawSenders(Canvas canvas) {
   1636         canvas.translate(mCoordinates.sendersX, mCoordinates.sendersY);
   1637         mSendersTextView.draw(canvas);
   1638     }
   1639 
   1640     private Bitmap getStarBitmap() {
   1641         return mHeader.conversation.starred ? STAR_ON : STAR_OFF;
   1642     }
   1643 
   1644     private static void drawText(Canvas canvas, CharSequence s, int x, int y, TextPaint paint) {
   1645         canvas.drawText(s, 0, s.length(), x, y, paint);
   1646     }
   1647 
   1648     /**
   1649      * Set the background for this item based on:
   1650      * 1. Read / Unread (unread messages have a lighter background)
   1651      * 2. Tablet / Phone
   1652      * 3. Checkbox checked / Unchecked (controls CAB color for item)
   1653      * 4. Activated / Not activated (controls the blue highlight on tablet)
   1654      * @param isUnread
   1655      */
   1656     private void updateBackground(boolean isUnread) {
   1657         final int background;
   1658         if (mBackgroundOverrideResId > 0) {
   1659             background = mBackgroundOverrideResId;
   1660         } else if (isUnread) {
   1661             background = R.drawable.conversation_unread_selector;
   1662         } else {
   1663             background = R.drawable.conversation_read_selector;
   1664         }
   1665         setBackgroundResource(background);
   1666     }
   1667 
   1668     /**
   1669      * Toggle the check mark on this view and update the conversation or begin
   1670      * drag, if drag is enabled.
   1671      */
   1672     @Override
   1673     public boolean toggleSelectedStateOrBeginDrag() {
   1674         ViewMode mode = mActivity.getViewMode();
   1675         if (mIsExpansiveTablet && mode.isListMode()) {
   1676             return beginDragMode();
   1677         } else {
   1678             return toggleSelectedState("long_press");
   1679         }
   1680     }
   1681 
   1682     @Override
   1683     public boolean toggleSelectedState() {
   1684         return toggleSelectedState(null);
   1685     }
   1686 
   1687     private boolean toggleSelectedState(String sourceOpt) {
   1688         if (mHeader != null && mHeader.conversation != null && mSelectedConversationSet != null) {
   1689             mSelected = !mSelected;
   1690             setSelected(mSelected);
   1691             Conversation conv = mHeader.conversation;
   1692             // Set the list position of this item in the conversation
   1693             SwipeableListView listView = getListView();
   1694 
   1695             try {
   1696                 conv.position = mSelected && listView != null ? listView.getPositionForView(this)
   1697                         : Conversation.NO_POSITION;
   1698             } catch (final NullPointerException e) {
   1699                 // TODO(skennedy) Remove this if we find the root cause b/9527863
   1700             }
   1701 
   1702             if (mSelectedConversationSet.isEmpty()) {
   1703                 final String source = (sourceOpt != null) ? sourceOpt : "checkbox";
   1704                 Analytics.getInstance().sendEvent("enter_cab_mode", source, null, 0);
   1705             }
   1706 
   1707             mSelectedConversationSet.toggle(conv);
   1708             if (mSelectedConversationSet.isEmpty()) {
   1709                 listView.commitDestructiveActions(true);
   1710             }
   1711 
   1712             final boolean reverse = !mSelected;
   1713             mPhotoFlipAnimator.startAnimation(reverse);
   1714             mPhotoFlipAnimator.invalidateArea();
   1715 
   1716             // We update the background after the checked state has changed
   1717             // now that we have a selected background asset. Setting the background
   1718             // usually waits for a layout pass, but we don't need a full layout,
   1719             // just an update to the background.
   1720             requestLayout();
   1721 
   1722             return true;
   1723         }
   1724 
   1725         return false;
   1726     }
   1727 
   1728     /**
   1729      * Toggle the star on this view and update the conversation.
   1730      */
   1731     public void toggleStar() {
   1732         mHeader.conversation.starred = !mHeader.conversation.starred;
   1733         Bitmap starBitmap = getStarBitmap();
   1734         postInvalidate(mCoordinates.starX, mCoordinates.starY, mCoordinates.starX
   1735                 + starBitmap.getWidth(),
   1736                 mCoordinates.starY + starBitmap.getHeight());
   1737         ConversationCursor cursor = (ConversationCursor) mAdapter.getCursor();
   1738         if (cursor != null) {
   1739             // TODO(skennedy) What about ads?
   1740             cursor.updateBoolean(mHeader.conversation, ConversationColumns.STARRED,
   1741                     mHeader.conversation.starred);
   1742         }
   1743     }
   1744 
   1745     private boolean isTouchInContactPhoto(float x, float y) {
   1746         // Everything before the right edge of contact photo
   1747 
   1748         final int threshold = mCoordinates.contactImagesX + mCoordinates.contactImagesWidth
   1749                 + sSenderImageTouchSlop;
   1750 
   1751         // Allow touching a little right of the contact photo when we're already in selection mode
   1752         final float extra;
   1753         if (mSelectedConversationSet == null || mSelectedConversationSet.isEmpty()) {
   1754             extra = 0;
   1755         } else {
   1756             extra = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16,
   1757                     getResources().getDisplayMetrics());
   1758         }
   1759 
   1760         return mHeader.gadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO
   1761                 && x < (threshold + extra)
   1762                 && (!isAttachmentPreviewsEnabled() || y < mCoordinates.attachmentPreviewsY);
   1763     }
   1764 
   1765     private boolean isTouchInInfoIcon(final float x, final float y) {
   1766         if (mHeader.infoIcon == null) {
   1767             // We have no info icon
   1768             return false;
   1769         }
   1770 
   1771         // Regardless of device, we always want to be right of the date's left touch slop
   1772         if (x < mDateX - sStarTouchSlop) {
   1773             return false;
   1774         }
   1775 
   1776         if (mStarEnabled) {
   1777             if (mIsExpansiveTablet) {
   1778                 // Just check that we're left of the star's touch area
   1779                 if (x >= mCoordinates.starX - sStarTouchSlop) {
   1780                     return false;
   1781                 }
   1782             } else {
   1783                 // We're on a phone or non-expansive tablet
   1784 
   1785                 // We allow touches all the way to the right edge, so no x check is necessary
   1786 
   1787                 // We need to be above the star's touch area, which ends at the top of the subject
   1788                 // text
   1789                 return y < mCoordinates.subjectY;
   1790             }
   1791         }
   1792 
   1793         // With no star below the info icon, we allow touches anywhere from the top edge to the
   1794         // bottom edge, or to the top of the attachment previews, whichever is higher
   1795         return !isAttachmentPreviewsEnabled() || y < mCoordinates.attachmentPreviewsY;
   1796     }
   1797 
   1798     private boolean isTouchInStar(float x, float y) {
   1799         if (mHeader.infoIcon != null && !mIsExpansiveTablet) {
   1800             // We have an info icon, and it's above the star
   1801             // We allow touches everywhere below the top of the subject text
   1802             if (y < mCoordinates.subjectY) {
   1803                 return false;
   1804             }
   1805         }
   1806 
   1807         // Everything after the star and include a touch slop.
   1808         return mStarEnabled
   1809                 && x > mCoordinates.starX - sStarTouchSlop
   1810                 && (!isAttachmentPreviewsEnabled() || y < mCoordinates.attachmentPreviewsY);
   1811     }
   1812 
   1813     @Override
   1814     public boolean canChildBeDismissed() {
   1815         return true;
   1816     }
   1817 
   1818     @Override
   1819     public void dismiss() {
   1820         SwipeableListView listView = getListView();
   1821         if (listView != null) {
   1822             getListView().dismissChild(this);
   1823         }
   1824     }
   1825 
   1826     private boolean onTouchEventNoSwipe(MotionEvent event) {
   1827         Utils.traceBeginSection("on touch event no swipe");
   1828         boolean handled = false;
   1829 
   1830         int x = (int) event.getX();
   1831         int y = (int) event.getY();
   1832         mLastTouchX = x;
   1833         mLastTouchY = y;
   1834         switch (event.getAction()) {
   1835             case MotionEvent.ACTION_DOWN:
   1836                 if (isTouchInContactPhoto(x, y) || isTouchInInfoIcon(x, y) || isTouchInStar(x, y)) {
   1837                     mDownEvent = true;
   1838                     handled = true;
   1839                 }
   1840                 break;
   1841 
   1842             case MotionEvent.ACTION_CANCEL:
   1843                 mDownEvent = false;
   1844                 break;
   1845 
   1846             case MotionEvent.ACTION_UP:
   1847                 if (mDownEvent) {
   1848                     if (isTouchInContactPhoto(x, y)) {
   1849                         // Touch on the check mark
   1850                         toggleSelectedState();
   1851                     } else if (isTouchInInfoIcon(x, y)) {
   1852                         if (mConversationItemAreaClickListener != null) {
   1853                             mConversationItemAreaClickListener.onInfoIconClicked();
   1854                         }
   1855                     } else if (isTouchInStar(x, y)) {
   1856                         // Touch on the star
   1857                         if (mConversationItemAreaClickListener == null) {
   1858                             toggleStar();
   1859                         } else {
   1860                             mConversationItemAreaClickListener.onStarClicked();
   1861                         }
   1862                     }
   1863                     handled = true;
   1864                 }
   1865                 break;
   1866         }
   1867 
   1868         if (!handled) {
   1869             handled = super.onTouchEvent(event);
   1870         }
   1871 
   1872         Utils.traceEndSection();
   1873         return handled;
   1874     }
   1875 
   1876     /**
   1877      * ConversationItemView is given the first chance to handle touch events.
   1878      */
   1879     @Override
   1880     public boolean onTouchEvent(MotionEvent event) {
   1881         Utils.traceBeginSection("on touch event");
   1882         int x = (int) event.getX();
   1883         int y = (int) event.getY();
   1884         mLastTouchX = x;
   1885         mLastTouchY = y;
   1886         if (!mSwipeEnabled) {
   1887             Utils.traceEndSection();
   1888             return onTouchEventNoSwipe(event);
   1889         }
   1890         switch (event.getAction()) {
   1891             case MotionEvent.ACTION_DOWN:
   1892                 if (isTouchInContactPhoto(x, y) || isTouchInInfoIcon(x, y) || isTouchInStar(x, y)) {
   1893                     mDownEvent = true;
   1894                     Utils.traceEndSection();
   1895                     return true;
   1896                 }
   1897                 break;
   1898             case MotionEvent.ACTION_UP:
   1899                 if (mDownEvent) {
   1900                     if (isTouchInContactPhoto(x, y)) {
   1901                         // Touch on the check mark
   1902                         Utils.traceEndSection();
   1903                         mDownEvent = false;
   1904                         toggleSelectedState();
   1905                         Utils.traceEndSection();
   1906                         return true;
   1907                     } else if (isTouchInInfoIcon(x, y)) {
   1908                         // Touch on the info icon
   1909                         mDownEvent = false;
   1910                         if (mConversationItemAreaClickListener != null) {
   1911                             mConversationItemAreaClickListener.onInfoIconClicked();
   1912                         }
   1913                         Utils.traceEndSection();
   1914                         return true;
   1915                     } else if (isTouchInStar(x, y)) {
   1916                         // Touch on the star
   1917                         mDownEvent = false;
   1918                         if (mConversationItemAreaClickListener == null) {
   1919                             toggleStar();
   1920                         } else {
   1921                             mConversationItemAreaClickListener.onStarClicked();
   1922                         }
   1923                         Utils.traceEndSection();
   1924                         return true;
   1925                     }
   1926                 }
   1927                 break;
   1928         }
   1929         // Let View try to handle it as well.
   1930         boolean handled = super.onTouchEvent(event);
   1931         if (event.getAction() == MotionEvent.ACTION_DOWN) {
   1932             Utils.traceEndSection();
   1933             return true;
   1934         }
   1935         Utils.traceEndSection();
   1936         return handled;
   1937     }
   1938 
   1939     @Override
   1940     public boolean performClick() {
   1941         final boolean handled = super.performClick();
   1942         final SwipeableListView list = getListView();
   1943         if (!handled && list != null && list.getAdapter() != null) {
   1944             final int pos = list.findConversation(this, mHeader.conversation);
   1945             list.performItemClick(this, pos, mHeader.conversation.id);
   1946         }
   1947         return handled;
   1948     }
   1949 
   1950     private View unwrap() {
   1951         final ViewParent vp = getParent();
   1952         if (vp == null || !(vp instanceof View)) {
   1953             return null;
   1954         }
   1955         return (View) vp;
   1956     }
   1957 
   1958     private SwipeableListView getListView() {
   1959         SwipeableListView v = null;
   1960         final View wrapper = unwrap();
   1961         if (wrapper != null && wrapper instanceof SwipeableConversationItemView) {
   1962             v = (SwipeableListView) ((SwipeableConversationItemView) wrapper).getListView();
   1963         }
   1964         if (v == null) {
   1965             v = mAdapter.getListView();
   1966         }
   1967         return v;
   1968     }
   1969 
   1970     /**
   1971      * Reset any state associated with this conversation item view so that it
   1972      * can be reused.
   1973      */
   1974     public void reset() {
   1975         Utils.traceBeginSection("reset");
   1976         setAlpha(1f);
   1977         setTranslationX(0f);
   1978         mAnimatedHeightFraction = 1.0f;
   1979         Utils.traceEndSection();
   1980     }
   1981 
   1982     @SuppressWarnings("deprecation")
   1983     @Override
   1984     public void setTranslationX(float translationX) {
   1985         super.setTranslationX(translationX);
   1986 
   1987         // When a list item is being swiped or animated, ensure that the hosting view has a
   1988         // background color set. We only enable the background during the X-translation effect to
   1989         // reduce overdraw during normal list scrolling.
   1990         final View parent = (View) getParent();
   1991         if (parent == null) {
   1992             LogUtils.w(LOG_TAG, "CIV.setTranslationX null ConversationItemView parent x=%s",
   1993                     translationX);
   1994         }
   1995 
   1996         if (parent instanceof SwipeableConversationItemView) {
   1997             if (translationX != 0f) {
   1998                 parent.setBackgroundResource(R.color.swiped_bg_color);
   1999             } else {
   2000                 parent.setBackgroundDrawable(null);
   2001             }
   2002         }
   2003     }
   2004 
   2005     /**
   2006      * Grow the height of the item and fade it in when bringing a conversation
   2007      * back from a destructive action.
   2008      */
   2009     public Animator createSwipeUndoAnimation() {
   2010         ObjectAnimator undoAnimator = createTranslateXAnimation(true);
   2011         return undoAnimator;
   2012     }
   2013 
   2014     /**
   2015      * Grow the height of the item and fade it in when bringing a conversation
   2016      * back from a destructive action.
   2017      */
   2018     public Animator createUndoAnimation() {
   2019         ObjectAnimator height = createHeightAnimation(true);
   2020         Animator fade = ObjectAnimator.ofFloat(this, "alpha", 0, 1.0f);
   2021         fade.setDuration(sShrinkAnimationDuration);
   2022         fade.setInterpolator(new DecelerateInterpolator(2.0f));
   2023         AnimatorSet transitionSet = new AnimatorSet();
   2024         transitionSet.playTogether(height, fade);
   2025         transitionSet.addListener(new HardwareLayerEnabler(this));
   2026         return transitionSet;
   2027     }
   2028 
   2029     /**
   2030      * Grow the height of the item and fade it in when bringing a conversation
   2031      * back from a destructive action.
   2032      */
   2033     public Animator createDestroyWithSwipeAnimation() {
   2034         ObjectAnimator slide = createTranslateXAnimation(false);
   2035         ObjectAnimator height = createHeightAnimation(false);
   2036         AnimatorSet transitionSet = new AnimatorSet();
   2037         transitionSet.playSequentially(slide, height);
   2038         return transitionSet;
   2039     }
   2040 
   2041     private ObjectAnimator createTranslateXAnimation(boolean show) {
   2042         SwipeableListView parent = getListView();
   2043         // If we can't get the parent...we have bigger problems.
   2044         int width = parent != null ? parent.getMeasuredWidth() : 0;
   2045         final float start = show ? width : 0f;
   2046         final float end = show ? 0f : width;
   2047         ObjectAnimator slide = ObjectAnimator.ofFloat(this, "translationX", start, end);
   2048         slide.setInterpolator(new DecelerateInterpolator(2.0f));
   2049         slide.setDuration(sSlideAnimationDuration);
   2050         return slide;
   2051     }
   2052 
   2053     public Animator createDestroyAnimation() {
   2054         return createHeightAnimation(false);
   2055     }
   2056 
   2057     private ObjectAnimator createHeightAnimation(boolean show) {
   2058         final float start = show ? 0f : 1.0f;
   2059         final float end = show ? 1.0f : 0f;
   2060         ObjectAnimator height = ObjectAnimator.ofFloat(this, "animatedHeightFraction", start, end);
   2061         height.setInterpolator(new DecelerateInterpolator(2.0f));
   2062         height.setDuration(sShrinkAnimationDuration);
   2063         return height;
   2064     }
   2065 
   2066     // Used by animator
   2067     public void setAnimatedHeightFraction(float height) {
   2068         mAnimatedHeightFraction = height;
   2069         requestLayout();
   2070     }
   2071 
   2072     @Override
   2073     public SwipeableView getSwipeableView() {
   2074         return SwipeableView.from(this);
   2075     }
   2076 
   2077     /**
   2078      * Begin drag mode. Keep the conversation selected (NOT toggle selection) and start drag.
   2079      */
   2080     private boolean beginDragMode() {
   2081         if (mLastTouchX < 0 || mLastTouchY < 0 ||  mSelectedConversationSet == null) {
   2082             return false;
   2083         }
   2084         // If this is already checked, don't bother unchecking it!
   2085         if (!mSelected) {
   2086             toggleSelectedState();
   2087         }
   2088 
   2089         // Clip data has form: [conversations_uri, conversationId1,
   2090         // maxMessageId1, label1, conversationId2, maxMessageId2, label2, ...]
   2091         final int count = mSelectedConversationSet.size();
   2092         String description = Utils.formatPlural(mContext, R.plurals.move_conversation, count);
   2093 
   2094         final ClipData data = ClipData.newUri(mContext.getContentResolver(), description,
   2095                 Conversation.MOVE_CONVERSATIONS_URI);
   2096         for (Conversation conversation : mSelectedConversationSet.values()) {
   2097             data.addItem(new Item(String.valueOf(conversation.position)));
   2098         }
   2099         // Protect against non-existent views: only happens for monkeys
   2100         final int width = this.getWidth();
   2101         final int height = this.getHeight();
   2102         final boolean isDimensionNegative = (width < 0) || (height < 0);
   2103         if (isDimensionNegative) {
   2104             LogUtils.e(LOG_TAG, "ConversationItemView: dimension is negative: "
   2105                         + "width=%d, height=%d", width, height);
   2106             return false;
   2107         }
   2108         mActivity.startDragMode();
   2109         // Start drag mode
   2110         startDrag(data, new ShadowBuilder(this, count, mLastTouchX, mLastTouchY), null, 0);
   2111 
   2112         return true;
   2113     }
   2114 
   2115     /**
   2116      * Handles the drag event.
   2117      *
   2118      * @param event the drag event to be handled
   2119      */
   2120     @Override
   2121     public boolean onDragEvent(DragEvent event) {
   2122         switch (event.getAction()) {
   2123             case DragEvent.ACTION_DRAG_ENDED:
   2124                 mActivity.stopDragMode();
   2125                 return true;
   2126         }
   2127         return false;
   2128     }
   2129 
   2130     private class ShadowBuilder extends DragShadowBuilder {
   2131         private final Drawable mBackground;
   2132 
   2133         private final View mView;
   2134         private final String mDragDesc;
   2135         private final int mTouchX;
   2136         private final int mTouchY;
   2137         private int mDragDescX;
   2138         private int mDragDescY;
   2139 
   2140         public ShadowBuilder(View view, int count, int touchX, int touchY) {
   2141             super(view);
   2142             mView = view;
   2143             mBackground = mView.getResources().getDrawable(R.drawable.list_pressed_holo);
   2144             mDragDesc = Utils.formatPlural(mView.getContext(), R.plurals.move_conversation, count);
   2145             mTouchX = touchX;
   2146             mTouchY = touchY;
   2147         }
   2148 
   2149         @Override
   2150         public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint) {
   2151             final int width = mView.getWidth();
   2152             final int height = mView.getHeight();
   2153 
   2154             sPaint.setTextSize(mCoordinates.subjectFontSize);
   2155             mDragDescX = mCoordinates.sendersX;
   2156             mDragDescY = (height - (int) mCoordinates.subjectFontSize) / 2 ;
   2157             shadowSize.set(width, height);
   2158             shadowTouchPoint.set(mTouchX, mTouchY);
   2159         }
   2160 
   2161         @Override
   2162         public void onDrawShadow(Canvas canvas) {
   2163             mBackground.setBounds(0, 0, mView.getWidth(), mView.getHeight());
   2164             mBackground.draw(canvas);
   2165             sPaint.setTextSize(mCoordinates.subjectFontSize);
   2166             canvas.drawText(mDragDesc, mDragDescX, mDragDescY - sPaint.ascent(), sPaint);
   2167         }
   2168     }
   2169 
   2170     @Override
   2171     public float getMinAllowScrollDistance() {
   2172         return sScrollSlop;
   2173     }
   2174 
   2175     private abstract class CabAnimator {
   2176         private ObjectAnimator mAnimator = null;
   2177 
   2178         private final String mPropertyName;
   2179 
   2180         private float mValue;
   2181 
   2182         private final float mStartValue;
   2183         private final float mEndValue;
   2184 
   2185         private final long mDuration;
   2186 
   2187         private boolean mReversing = false;
   2188 
   2189         public CabAnimator(final String propertyName, final float startValue, final float endValue,
   2190                 final long duration) {
   2191             mPropertyName = propertyName;
   2192 
   2193             mStartValue = startValue;
   2194             mEndValue = endValue;
   2195 
   2196             mDuration = duration;
   2197         }
   2198 
   2199         private ObjectAnimator createAnimator() {
   2200             final ObjectAnimator animator = ObjectAnimator.ofFloat(ConversationItemView.this,
   2201                     mPropertyName, mStartValue, mEndValue);
   2202             animator.setDuration(mDuration);
   2203             animator.setInterpolator(new LinearInterpolator());
   2204             animator.addListener(new AnimatorListenerAdapter() {
   2205                 @Override
   2206                 public void onAnimationEnd(final Animator animation) {
   2207                     invalidateArea();
   2208                 }
   2209             });
   2210             animator.addListener(mAnimatorListener);
   2211             return animator;
   2212         }
   2213 
   2214         private final AnimatorListener mAnimatorListener = new AnimatorListener() {
   2215             @Override
   2216             public void onAnimationStart(final Animator animation) {
   2217                 // Do nothing
   2218             }
   2219 
   2220             @Override
   2221             public void onAnimationEnd(final Animator animation) {
   2222                 if (mReversing) {
   2223                     mReversing = false;
   2224                     // We no longer want to track whether we were last selected,
   2225                     // since we no longer are selected
   2226                     mLastSelectedId = -1;
   2227                 }
   2228             }
   2229 
   2230             @Override
   2231             public void onAnimationCancel(final Animator animation) {
   2232                 // Do nothing
   2233             }
   2234 
   2235             @Override
   2236             public void onAnimationRepeat(final Animator animation) {
   2237                 // Do nothing
   2238             }
   2239         };
   2240 
   2241         public abstract void invalidateArea();
   2242 
   2243         public void setValue(final float fraction) {
   2244             if (mValue == fraction) {
   2245                 return;
   2246             }
   2247             mValue = fraction;
   2248             invalidateArea();
   2249         }
   2250 
   2251         public float getValue() {
   2252             return mValue;
   2253         }
   2254 
   2255         /**
   2256          * @param reverse <code>true</code> to animate in reverse
   2257          */
   2258         public void startAnimation(final boolean reverse) {
   2259             if (mAnimator != null) {
   2260                 mAnimator.cancel();
   2261             }
   2262 
   2263             mAnimator = createAnimator();
   2264             mReversing = reverse;
   2265 
   2266             if (reverse) {
   2267                 mAnimator.reverse();
   2268             } else {
   2269                 mAnimator.start();
   2270             }
   2271         }
   2272 
   2273         public void stopAnimation() {
   2274             if (mAnimator != null) {
   2275                 mAnimator.cancel();
   2276                 mAnimator = null;
   2277             }
   2278 
   2279             mReversing = false;
   2280 
   2281             setValue(0);
   2282         }
   2283 
   2284         public boolean isStarted() {
   2285             return mAnimator != null && mAnimator.isStarted();
   2286         }
   2287     }
   2288 
   2289     public void setPhotoFlipFraction(final float fraction) {
   2290         mPhotoFlipAnimator.setValue(fraction);
   2291     }
   2292 
   2293     public String getAccount() {
   2294         return mAccount;
   2295     }
   2296 }
   2297