ConversationItemView.java revision 370f868c834861e7732faaa9bdd07a0fa0105596
1/*
2 * Copyright (C) 2012 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.mail.browse;
19
20import android.animation.Animator;
21import android.animation.AnimatorSet;
22import android.animation.ObjectAnimator;
23import android.content.ClipData;
24import android.content.ClipData.Item;
25import android.content.Context;
26import android.content.res.Resources;
27import android.database.Cursor;
28import android.graphics.Bitmap;
29import android.graphics.BitmapFactory;
30import android.graphics.Canvas;
31import android.graphics.LinearGradient;
32import android.graphics.Paint;
33import android.graphics.Point;
34import android.graphics.Shader;
35import android.graphics.Typeface;
36import android.graphics.drawable.Drawable;
37import android.net.Uri;
38import android.text.Layout.Alignment;
39import android.text.Spannable;
40import android.text.SpannableString;
41import android.text.SpannableStringBuilder;
42import android.text.StaticLayout;
43import android.text.TextPaint;
44import android.text.TextUtils;
45import android.text.TextUtils.TruncateAt;
46import android.text.format.DateUtils;
47import android.text.style.CharacterStyle;
48import android.text.style.ForegroundColorSpan;
49import android.text.style.TextAppearanceSpan;
50import android.util.SparseArray;
51import android.util.TypedValue;
52import android.view.DragEvent;
53import android.view.MotionEvent;
54import android.view.View;
55import android.view.ViewGroup;
56import android.view.ViewParent;
57import android.view.animation.DecelerateInterpolator;
58import android.widget.TextView;
59
60import com.android.mail.R;
61import com.android.mail.browse.ConversationItemViewModel.SenderFragment;
62import com.android.mail.perf.Timer;
63import com.android.mail.photomanager.ContactPhotoManager;
64import com.android.mail.photomanager.ContactPhotoManager.ContactIdentifier;
65import com.android.mail.photomanager.PhotoManager.PhotoIdentifier;
66import com.android.mail.providers.Conversation;
67import com.android.mail.providers.Folder;
68import com.android.mail.providers.UIProvider;
69import com.android.mail.providers.UIProvider.ConversationListIcon;
70import com.android.mail.providers.UIProvider.ConversationColumns;
71import com.android.mail.providers.UIProvider.FolderType;
72import com.android.mail.ui.AnimatedAdapter;
73import com.android.mail.ui.ControllableActivity;
74import com.android.mail.ui.ConversationSelectionSet;
75import com.android.mail.ui.DividedImageCanvas;
76import com.android.mail.ui.DividedImageCanvas.InvalidateCallback;
77import com.android.mail.ui.FolderDisplayer;
78import com.android.mail.ui.SwipeableItemView;
79import com.android.mail.ui.SwipeableListView;
80import com.android.mail.ui.ViewMode;
81import com.android.mail.utils.HardwareLayerEnabler;
82import com.android.mail.utils.LogTag;
83import com.android.mail.utils.LogUtils;
84import com.android.mail.utils.Utils;
85import com.google.common.annotations.VisibleForTesting;
86
87import java.util.ArrayList;
88
89public class ConversationItemView extends View implements SwipeableItemView, ToggleableItem,
90        InvalidateCallback {
91    // Timer.
92    private static int sLayoutCount = 0;
93    private static Timer sTimer; // Create the sTimer here if you need to do
94                                 // perf analysis.
95    private static final int PERF_LAYOUT_ITERATIONS = 50;
96    private static final String PERF_TAG_LAYOUT = "CCHV.layout";
97    private static final String PERF_TAG_CALCULATE_TEXTS_BITMAPS = "CCHV.txtsbmps";
98    private static final String PERF_TAG_CALCULATE_SENDER_SUBJECT = "CCHV.sendersubj";
99    private static final String PERF_TAG_CALCULATE_FOLDERS = "CCHV.folders";
100    private static final String PERF_TAG_CALCULATE_COORDINATES = "CCHV.coordinates";
101    private static final String LOG_TAG = LogTag.getLogTag();
102
103    // Static bitmaps.
104    private static Bitmap CHECKMARK_OFF;
105    private static Bitmap CHECKMARK_ON;
106    private static Bitmap STAR_OFF;
107    private static Bitmap STAR_ON;
108    private static Bitmap ATTACHMENT;
109    private static Bitmap STATE_REPLIED;
110    private static Bitmap STATE_FORWARDED;
111    private static Bitmap STATE_REPLIED_AND_FORWARDED;
112    private static Bitmap STATE_CALENDAR_INVITE;
113
114    private static String sSendersSplitToken;
115    private static String sElidedPaddingToken;
116
117    // Static colors.
118    private static int sActivatedTextColor;
119    private static int sSendersTextColorRead;
120    private static int sSendersTextColorUnread;
121    private static int sDateTextColor;
122    private static int sTouchSlop;
123    @Deprecated
124    private static int sStandardScaledDimen;
125    private static int sShrinkAnimationDuration;
126    private static int sSlideAnimationDuration;
127    private static int sAnimatingBackgroundColor;
128
129    // Static paints.
130    private static TextPaint sPaint = new TextPaint();
131    private static TextPaint sFoldersPaint = new TextPaint();
132
133    // Backgrounds for different states.
134    private final SparseArray<Drawable> mBackgrounds = new SparseArray<Drawable>();
135
136    // Dimensions and coordinates.
137    private int mViewWidth = -1;
138    /** The view mode at which we calculated mViewWidth previously. */
139    private int mPreviousMode;
140    private int mMode = -1;
141    private int mDateX;
142    private int mPaperclipX;
143    private int mSendersWidth;
144
145    /** Whether we're running under test mode. */
146    private boolean mTesting = false;
147    /** Whether we are on a tablet device or not */
148    private final boolean mTabletDevice;
149
150    @VisibleForTesting
151    ConversationItemViewCoordinates mCoordinates;
152
153    private ConversationItemViewCoordinates.Config mConfig;
154
155    private final Context mContext;
156
157    public ConversationItemViewModel mHeader;
158    private boolean mDownEvent;
159    private boolean mChecked = false;
160    private ConversationSelectionSet mSelectedConversationSet;
161    private Folder mDisplayedFolder;
162    private boolean mCheckboxesEnabled;
163    private boolean mStarEnabled;
164    private boolean mSwipeEnabled;
165    private int mLastTouchX;
166    private int mLastTouchY;
167    private AnimatedAdapter mAdapter;
168    private float mAnimatedHeightFraction = 1.0f;
169    private final String mAccount;
170    private ControllableActivity mActivity;
171    private final TextView mSubjectTextView;
172    private final TextView mSendersTextView;
173    private boolean mConvListPhotosEnabled;
174    private final DividedImageCanvas mContactImagesHolder;
175    private int mAttachmentPreviewMode;
176    private final DividedImageCanvas mAttachmentPreviewsCanvas;
177
178    private static int sFoldersLeftPadding;
179    private static TextAppearanceSpan sSubjectTextUnreadSpan;
180    private static TextAppearanceSpan sSubjectTextReadSpan;
181    private static ForegroundColorSpan sSnippetTextUnreadSpan;
182    private static ForegroundColorSpan sSnippetTextReadSpan;
183    private static int sScrollSlop;
184    private static CharacterStyle sActivatedTextSpan;
185    private static ContactPhotoManager sContactPhotoManager;
186    private static ContactPhotoManager sAttachmentPreviewsManager;
187    private static final String EMPTY_SNIPPET = "";
188
189    static {
190        sPaint.setAntiAlias(true);
191        sFoldersPaint.setAntiAlias(true);
192    }
193
194    /**
195     * Handles displaying folders in a conversation header view.
196     */
197    static class ConversationItemFolderDisplayer extends FolderDisplayer {
198
199        private int mFoldersCount;
200
201        public ConversationItemFolderDisplayer(Context context) {
202            super(context);
203        }
204
205        @Override
206        public void loadConversationFolders(Conversation conv, final Uri ignoreFolderUri,
207                final int ignoreFolderType) {
208            super.loadConversationFolders(conv, ignoreFolderUri, ignoreFolderType);
209            mFoldersCount = mFoldersSortedSet.size();
210        }
211
212        @Override
213        public void reset() {
214            super.reset();
215            mFoldersCount = 0;
216        }
217
218        public boolean hasVisibleFolders() {
219            return mFoldersCount > 0;
220        }
221
222        private int measureFolders(int mode, int availableSpace, int cellSize) {
223            int totalWidth = 0;
224            boolean firstTime = true;
225            for (Folder f : mFoldersSortedSet) {
226                final String folderString = f.name;
227                int width = (int) sFoldersPaint.measureText(folderString) + cellSize;
228                if (firstTime) {
229                    firstTime = false;
230                } else {
231                    width += sFoldersLeftPadding;
232                }
233                totalWidth += width;
234                if (totalWidth > availableSpace) {
235                    break;
236                }
237            }
238
239            return totalWidth;
240        }
241
242        public void drawFolders(Canvas canvas, ConversationItemViewCoordinates coordinates,
243                int mode) {
244            if (mFoldersCount == 0) {
245                return;
246            }
247            final int xMinStart = coordinates.foldersX;
248            final int xEnd = coordinates.foldersXEnd;
249            final int y = coordinates.foldersY;
250            final int height = coordinates.foldersHeight;
251            final int ascent = coordinates.foldersAscent;
252            int textBottomPadding = coordinates.foldersTextBottomPadding;
253
254            sFoldersPaint.setTextSize(coordinates.foldersFontSize);
255            sFoldersPaint.setTypeface(coordinates.foldersTypeface);
256
257            // Initialize space and cell size based on the current mode.
258            int availableSpace = xEnd - xMinStart;
259            int averageWidth = availableSpace / mFoldersCount;
260            int cellSize = ConversationItemViewCoordinates.getFolderCellWidth(mContext);
261
262            // TODO(ath): sFoldersPaint.measureText() is done 3x in this method. stop that.
263            // Extra credit: maybe cache results across items as long as font size doesn't change.
264
265            final int totalWidth = measureFolders(mode, availableSpace, cellSize);
266            int xStart = xEnd - Math.min(availableSpace, totalWidth);
267            final boolean overflow = totalWidth > availableSpace;
268
269            // Second pass to draw folders.
270            int i = 0;
271            for (Folder f : mFoldersSortedSet) {
272                if (availableSpace <= 0) {
273                    break;
274                }
275                final String folderString = f.name;
276                final int fgColor = f.getForegroundColor(mDefaultFgColor);
277                final int bgColor = f.getBackgroundColor(mDefaultBgColor);
278                boolean labelTooLong = false;
279                final int textW = (int) sFoldersPaint.measureText(folderString);
280                int width = textW + cellSize + sFoldersLeftPadding;
281
282                if (overflow && width > averageWidth) {
283                    if (i < mFoldersCount - 1) {
284                        width = averageWidth;
285                    } else {
286                        // allow the last label to take all remaining space
287                        // (and don't let it make room for padding)
288                        width = availableSpace + sFoldersLeftPadding;
289                    }
290                    labelTooLong = true;
291                }
292
293                // TODO (mindyp): how to we get this?
294                final boolean isMuted = false;
295                // labelValues.folderId ==
296                // sGmail.getFolderMap(mAccount).getFolderIdIgnored();
297
298                // Draw the box.
299                sFoldersPaint.setColor(bgColor);
300                sFoldersPaint.setStyle(isMuted ? Paint.Style.STROKE : Paint.Style.FILL_AND_STROKE);
301                canvas.drawRect(xStart, y, xStart + width - sFoldersLeftPadding,
302                        y + height, sFoldersPaint);
303
304                // Draw the text.
305                final int padding = cellSize / 2;
306                sFoldersPaint.setColor(fgColor);
307                if (labelTooLong) {
308                    final int rightBorder = xStart + width - sFoldersLeftPadding - padding;
309                    final Shader shader = new LinearGradient(rightBorder - padding, y, rightBorder,
310                            y, fgColor, Utils.getTransparentColor(fgColor), Shader.TileMode.CLAMP);
311                    sFoldersPaint.setShader(shader);
312                }
313                canvas.drawText(folderString, xStart + padding, y + height - textBottomPadding,
314                        sFoldersPaint);
315                if (labelTooLong) {
316                    sFoldersPaint.setShader(null);
317                }
318
319                availableSpace -= width;
320                xStart += width;
321                i++;
322            }
323        }
324    }
325
326    /**
327     * Helpers function to align an element in the center of a space.
328     */
329    private static int getPadding(int space, int length) {
330        return (space - length) / 2;
331    }
332
333    public ConversationItemView(Context context, String account) {
334        super(context);
335        setClickable(true);
336        setLongClickable(true);
337        mContext = context.getApplicationContext();
338        final Resources res = mContext.getResources();
339        mTabletDevice = Utils.useTabletUI(res);
340        mAccount = account;
341
342        if (CHECKMARK_OFF == null) {
343            // Initialize static bitmaps.
344            CHECKMARK_OFF = BitmapFactory.decodeResource(res,
345                    R.drawable.btn_check_off_normal_holo_light);
346            CHECKMARK_ON = BitmapFactory.decodeResource(res,
347                    R.drawable.btn_check_on_normal_holo_light);
348            STAR_OFF = BitmapFactory.decodeResource(res,
349                    R.drawable.btn_star_off_normal_email_holo_light);
350            STAR_ON = BitmapFactory.decodeResource(res,
351                    R.drawable.btn_star_on_normal_email_holo_light);
352            ATTACHMENT = BitmapFactory.decodeResource(res, R.drawable.ic_attachment_holo_light);
353            STATE_REPLIED =
354                    BitmapFactory.decodeResource(res, R.drawable.ic_badge_reply_holo_light);
355            STATE_FORWARDED =
356                    BitmapFactory.decodeResource(res, R.drawable.ic_badge_forward_holo_light);
357            STATE_REPLIED_AND_FORWARDED =
358                    BitmapFactory.decodeResource(res, R.drawable.ic_badge_reply_forward_holo_light);
359            STATE_CALENDAR_INVITE =
360                    BitmapFactory.decodeResource(res, R.drawable.ic_badge_invite_holo_light);
361
362            // Initialize colors.
363            sActivatedTextColor = res.getColor(android.R.color.white);
364            sActivatedTextSpan = CharacterStyle.wrap(new ForegroundColorSpan(sActivatedTextColor));
365            sSendersTextColorRead = res.getColor(R.color.senders_text_color_read);
366            sSendersTextColorUnread = res.getColor(R.color.senders_text_color_unread);
367            sSubjectTextUnreadSpan = new TextAppearanceSpan(mContext,
368                    R.style.SubjectAppearanceUnreadStyle);
369            sSubjectTextReadSpan = new TextAppearanceSpan(mContext,
370                    R.style.SubjectAppearanceReadStyle);
371            sSnippetTextUnreadSpan =
372                    new ForegroundColorSpan(res.getColor(R.color.snippet_text_color_unread));
373            sSnippetTextReadSpan =
374                    new ForegroundColorSpan(res.getColor(R.color.snippet_text_color_read));
375            sDateTextColor = res.getColor(R.color.date_text_color);
376            sTouchSlop = res.getDimensionPixelSize(R.dimen.touch_slop);
377            sStandardScaledDimen = res.getDimensionPixelSize(R.dimen.standard_scaled_dimen);
378            sShrinkAnimationDuration = res.getInteger(R.integer.shrink_animation_duration);
379            sSlideAnimationDuration = res.getInteger(R.integer.slide_animation_duration);
380            // Initialize static color.
381            sSendersSplitToken = res.getString(R.string.senders_split_token);
382            sElidedPaddingToken = res.getString(R.string.elided_padding_token);
383            sAnimatingBackgroundColor = res.getColor(R.color.animating_item_background_color);
384            sScrollSlop = res.getInteger(R.integer.swipeScrollSlop);
385            sFoldersLeftPadding = res.getDimensionPixelOffset(R.dimen.folders_left_padding);
386            sContactPhotoManager = ContactPhotoManager.createContactPhotoManager(context);
387            sAttachmentPreviewsManager = ContactPhotoManager.createContactPhotoManager(context);
388        }
389
390        mSendersTextView = new TextView(mContext);
391        mSendersTextView.setEllipsize(TextUtils.TruncateAt.END);
392        mSendersTextView.setIncludeFontPadding(false);
393
394        mSubjectTextView = new TextView(mContext);
395        mSubjectTextView.setEllipsize(TextUtils.TruncateAt.END);
396        mSubjectTextView.setIncludeFontPadding(false);
397
398        mContactImagesHolder = new DividedImageCanvas(context, this);
399        mAttachmentPreviewsCanvas = new DividedImageCanvas(context, this);
400    }
401
402    public void bind(Cursor cursor, ControllableActivity activity, ConversationSelectionSet set,
403            Folder folder, int checkboxOrSenderImage, boolean swipeEnabled,
404            boolean priorityArrowEnabled, AnimatedAdapter adapter) {
405        bind(ConversationItemViewModel.forCursor(mAccount, cursor), activity, set, folder,
406                checkboxOrSenderImage, swipeEnabled, priorityArrowEnabled, adapter);
407    }
408
409    public void bind(Conversation conversation, ControllableActivity activity,
410            ConversationSelectionSet set, Folder folder, int checkboxOrSenderImage,
411            boolean swipeEnabled, boolean priorityArrowEnabled, AnimatedAdapter adapter) {
412        bind(ConversationItemViewModel.forConversation(mAccount, conversation), activity, set,
413                folder, checkboxOrSenderImage, swipeEnabled, priorityArrowEnabled, adapter);
414    }
415
416    private void bind(ConversationItemViewModel header, ControllableActivity activity,
417            ConversationSelectionSet set, Folder folder, int checkboxOrSenderImage,
418            boolean swipeEnabled, boolean priorityArrowEnabled, AnimatedAdapter adapter) {
419        // If this was previously bound to a conversation, remove any contact
420        // photo manager requests.
421        // TODO:MARKWEI attachment previews
422        if (mHeader != null) {
423            final ArrayList<String> divisionIds = mContactImagesHolder.getDivisionIds();
424            if (divisionIds != null) {
425                for (int pos = 0; pos < divisionIds.size(); pos++) {
426                    sContactPhotoManager.removePhoto(DividedImageCanvas.generateHash(
427                            mContactImagesHolder, pos, divisionIds.get(pos)));
428                }
429            }
430        }
431        mHeader = header;
432        mActivity = activity;
433        mSelectedConversationSet = set;
434        mDisplayedFolder = folder;
435        mCheckboxesEnabled = (checkboxOrSenderImage == ConversationListIcon.CHECKBOX);
436        mConvListPhotosEnabled = (checkboxOrSenderImage == ConversationListIcon.SENDER_IMAGE);
437        mStarEnabled = folder != null && !folder.isTrash();
438        mSwipeEnabled = swipeEnabled;
439        mAdapter = adapter;
440        if (mHeader.conversation.getAttachmentsCount() == 0) {
441            mAttachmentPreviewMode = ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_NONE;
442        } else {
443            mAttachmentPreviewMode = mHeader.conversation.read ?
444                    ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_SHORT
445                    : ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_TALL;
446        }
447
448        final int gadgetMode;
449        if (mConvListPhotosEnabled) {
450            gadgetMode = ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO;
451        } else if (mCheckboxesEnabled) {
452            gadgetMode = ConversationItemViewCoordinates.GADGET_CHECKBOX;
453        } else {
454            gadgetMode = ConversationItemViewCoordinates.GADGET_NONE;
455        }
456
457        // Initialize folder displayer.
458        if (mHeader.folderDisplayer == null) {
459            mHeader.folderDisplayer = new ConversationItemFolderDisplayer(mContext);
460        } else {
461            mHeader.folderDisplayer.reset();
462        }
463
464        final int ignoreFolderType;
465        if (mDisplayedFolder.isInbox()) {
466            ignoreFolderType = FolderType.INBOX;
467        } else {
468            ignoreFolderType = -1;
469        }
470
471        mHeader.folderDisplayer.loadConversationFolders(mHeader.conversation, mDisplayedFolder.uri,
472                ignoreFolderType);
473
474        mHeader.dateText = DateUtils.getRelativeTimeSpanString(mContext,
475                mHeader.conversation.dateMs);
476
477        mConfig = new ConversationItemViewCoordinates.Config()
478            .withGadget(gadgetMode)
479            .withAttachmentPreviews(mAttachmentPreviewMode);
480        if (header.folderDisplayer.hasVisibleFolders()) {
481            mConfig.showFolders();
482        }
483        if (header.hasBeenForwarded || header.hasBeenRepliedTo || header.isInvite) {
484            mConfig.showReplyState();
485        }
486        if (mHeader.conversation.color != 0) {
487            mConfig.showColorBlock();
488        }
489
490        setContentDescription();
491        requestLayout();
492    }
493
494    /**
495     * Get the Conversation object associated with this view.
496     */
497    public Conversation getConversation() {
498        return mHeader.conversation;
499    }
500
501    /**
502     * Sets the mode. Only used for testing.
503     */
504    @VisibleForTesting
505    void setMode(int mode) {
506        mMode = mode;
507        mTesting = true;
508    }
509
510    private static void startTimer(String tag) {
511        if (sTimer != null) {
512            sTimer.start(tag);
513        }
514    }
515
516    private static void pauseTimer(String tag) {
517        if (sTimer != null) {
518            sTimer.pause(tag);
519        }
520    }
521
522    @Override
523    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
524        final int wSize = MeasureSpec.getSize(widthMeasureSpec);
525
526        final int currentMode = mActivity.getViewMode().getMode();
527        if (wSize != mViewWidth || mPreviousMode != currentMode) {
528            mViewWidth = wSize;
529            mPreviousMode = currentMode;
530            if (!mTesting) {
531                mMode = ConversationItemViewCoordinates.getMode(mContext, mPreviousMode);
532            }
533        }
534        mHeader.viewWidth = mViewWidth;
535
536        mConfig.updateWidth(wSize).setMode(mMode);
537
538        Resources res = getResources();
539        mHeader.standardScaledDimen = res.getDimensionPixelOffset(R.dimen.standard_scaled_dimen);
540        if (mHeader.standardScaledDimen != sStandardScaledDimen) {
541            // Large Text has been toggle on/off. Update the static dimens.
542            sStandardScaledDimen = mHeader.standardScaledDimen;
543            ConversationItemViewCoordinates.refreshConversationDimens(mContext);
544        }
545
546        mCoordinates = ConversationItemViewCoordinates.forConfig(mContext, mConfig,
547                mAdapter.getCoordinatesCache());
548
549        final int h = (mAnimatedHeightFraction != 1.0f) ?
550                Math.round(mAnimatedHeightFraction * mCoordinates.height) : mCoordinates.height;
551        setMeasuredDimension(mConfig.getWidth(), h);
552    }
553
554    @Override
555    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
556        startTimer(PERF_TAG_LAYOUT);
557
558        super.onLayout(changed, left, top, right, bottom);
559
560        calculateTextsAndBitmaps();
561        calculateCoordinates();
562
563        // Subject.
564        createSubject(mHeader.unread);
565
566        if (!mHeader.isLayoutValid(mContext)) {
567            setContentDescription();
568        }
569        mHeader.validate(mContext);
570
571        pauseTimer(PERF_TAG_LAYOUT);
572        if (sTimer != null && ++sLayoutCount >= PERF_LAYOUT_ITERATIONS) {
573            sTimer.dumpResults();
574            sTimer = new Timer();
575            sLayoutCount = 0;
576        }
577    }
578
579    private void setContentDescription() {
580        if (mActivity.isAccessibilityEnabled()) {
581            mHeader.resetContentDescription();
582            setContentDescription(mHeader.getContentDescription(mContext));
583        }
584    }
585
586    @Override
587    public void setBackgroundResource(int resourceId) {
588        Drawable drawable = mBackgrounds.get(resourceId);
589        if (drawable == null) {
590            drawable = getResources().getDrawable(resourceId);
591            mBackgrounds.put(resourceId, drawable);
592        }
593        if (getBackground() != drawable) {
594            super.setBackgroundDrawable(drawable);
595        }
596    }
597
598    private void calculateTextsAndBitmaps() {
599        startTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
600
601        if (mSelectedConversationSet != null) {
602            mChecked = mSelectedConversationSet.contains(mHeader.conversation);
603        }
604        mHeader.checkboxVisible = mCheckboxesEnabled && !mConvListPhotosEnabled;
605
606        final boolean isUnread = mHeader.unread;
607        updateBackground(isUnread);
608
609        mHeader.sendersDisplayText = new SpannableStringBuilder();
610        mHeader.styledSendersString = new SpannableStringBuilder();
611
612        // Parse senders fragments.
613        if (mHeader.conversation.conversationInfo != null) {
614            Context context = getContext();
615            mHeader.messageInfoString = SendersView
616                    .createMessageInfo(context, mHeader.conversation, true);
617            int maxChars = ConversationItemViewCoordinates.getSendersLength(context,
618                    ConversationItemViewCoordinates.getMode(context, mActivity.getViewMode()),
619                    mHeader.conversation.hasAttachments);
620            mHeader.displayableSenderEmails = new ArrayList<String>();
621            mHeader.displayableSenderNames = new ArrayList<String>();
622            mHeader.styledSenders = new ArrayList<SpannableString>();
623            SendersView.format(context, mHeader.conversation.conversationInfo,
624                    mHeader.messageInfoString.toString(), maxChars, mHeader.styledSenders,
625                    mHeader.displayableSenderNames, mHeader.displayableSenderEmails, mAccount,
626                    true);
627            // If we have displayable senders, load their thumbnails
628            loadSenderImages();
629        } else {
630            SendersView.formatSenders(mHeader, getContext(), true);
631        }
632
633        if (mAttachmentPreviewMode != ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_NONE) {
634            loadAttachmentPreviews();
635        }
636
637        if (mHeader.isLayoutValid(mContext)) {
638            pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
639            return;
640        }
641        startTimer(PERF_TAG_CALCULATE_FOLDERS);
642
643
644        pauseTimer(PERF_TAG_CALCULATE_FOLDERS);
645
646        // Paper clip icon.
647        mHeader.paperclip = null;
648        if (mHeader.conversation.hasAttachments) {
649            mHeader.paperclip = ATTACHMENT;
650        }
651
652        startTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT);
653
654        pauseTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT);
655        pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
656    }
657
658    // FIXME(ath): maybe move this to bind(). the only dependency on layout is on tile W/H, which
659    // is immutable.
660    private void loadSenderImages() {
661        if (mConvListPhotosEnabled && mHeader.displayableSenderEmails != null
662                && mHeader.displayableSenderEmails.size() > 0) {
663            if (mCoordinates.contactImagesWidth <= 0 || mCoordinates.contactImagesHeight <= 0) {
664                LogUtils.w(LOG_TAG,
665                        "Contact image width(%d) or height(%d) is 0 for mode: (%d).",
666                        mCoordinates.contactImagesWidth, mCoordinates.contactImagesHeight, mMode);
667                return;
668            }
669            mContactImagesHolder.setDimensions(mCoordinates.contactImagesWidth,
670                    mCoordinates.contactImagesHeight);
671            mContactImagesHolder.setDivisionIds(mHeader.displayableSenderEmails);
672            int size = mHeader.displayableSenderEmails.size();
673            String emailAddress;
674            for (int i = 0; i < DividedImageCanvas.MAX_DIVISIONS && i < size; i++) {
675                emailAddress = mHeader.displayableSenderEmails.get(i);
676                PhotoIdentifier photoIdentifier = new ContactIdentifier(
677                        mHeader.displayableSenderNames.get(i), emailAddress, i);
678                sContactPhotoManager.loadThumbnail(photoIdentifier, mContactImagesHolder);
679            }
680        }
681    }
682
683    private void loadAttachmentPreviews() {
684        if (mAttachmentPreviewMode != ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_NONE) {
685            final int attachmentPreviewsHeight = ConversationItemViewCoordinates
686                    .getAttachmentPreviewsHeight(mContext, mAttachmentPreviewMode);
687            if (mCoordinates.attachmentPreviewsWidth <= 0 || attachmentPreviewsHeight <= 0) {
688                LogUtils.w(LOG_TAG,
689                        "Attachment preview width(%d) or height(%d) is 0 for mode: (%d,%d).",
690                        mCoordinates.attachmentPreviewsWidth, attachmentPreviewsHeight, mMode,
691                        mAttachmentPreviewMode);
692                return;
693            }
694            mAttachmentPreviewsCanvas.setDimensions(mCoordinates.attachmentPreviewsWidth,
695                    attachmentPreviewsHeight);
696            ArrayList<String> attachments = mHeader.conversation.getAttachments();
697            mAttachmentPreviewsCanvas.setDivisionIds(attachments);
698            int size = attachments.size();
699            for (int i = 0; i < DividedImageCanvas.MAX_DIVISIONS && i < size; i++) {
700                String attachment = attachments.get(i);
701                PhotoIdentifier photoIdentifier = new ContactIdentifier(
702                        attachment, attachment, i);
703                sAttachmentPreviewsManager.loadThumbnail(
704                        photoIdentifier, mAttachmentPreviewsCanvas);
705            }
706        }
707    }
708
709    private static int makeExactSpecForSize(int size) {
710        return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
711    }
712
713    private static void layoutViewExactly(View v, int w, int h) {
714        v.measure(makeExactSpecForSize(w), makeExactSpecForSize(h));
715        v.layout(0, 0, w, h);
716    }
717
718    private void layoutSenders() {
719        if (mHeader.styledSendersString != null) {
720            if (isActivated() && showActivatedText()) {
721                mHeader.styledSendersString.setSpan(sActivatedTextSpan, 0,
722                        mHeader.styledMessageInfoStringOffset, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
723            } else {
724                mHeader.styledSendersString.removeSpan(sActivatedTextSpan);
725            }
726
727            final int w = mSendersWidth;
728            final int h = mCoordinates.sendersHeight;
729            mSendersTextView.setLayoutParams(new ViewGroup.LayoutParams(w, h));
730            mSendersTextView.setMaxLines(mCoordinates.sendersLineCount);
731            mSendersTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCoordinates.sendersFontSize);
732            layoutViewExactly(mSendersTextView, w, h);
733
734            mSendersTextView.setText(mHeader.styledSendersString);
735        }
736    }
737
738    private void createSubject(final boolean isUnread) {
739        final String subject = filterTag(mHeader.conversation.subject);
740        final String snippet = mHeader.conversation.getSnippet();
741        final SpannableStringBuilder displayedStringBuilder = new SpannableStringBuilder(
742                Conversation.getSubjectAndSnippetForDisplay(mContext, subject, snippet));
743
744        // since spans affect text metrics, add spans to the string before measure/layout or fancy
745        // ellipsizing
746        final int subjectTextLength = (subject != null) ? subject.length() : 0;
747        if (!TextUtils.isEmpty(subject)) {
748            displayedStringBuilder.setSpan(TextAppearanceSpan.wrap(
749                    isUnread ? sSubjectTextUnreadSpan : sSubjectTextReadSpan), 0, subjectTextLength,
750                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
751        }
752        if (!TextUtils.isEmpty(snippet)) {
753            final int startOffset = subjectTextLength;
754            // Start after the end of the subject text; since the subject may be
755            // "" or null, this could start at the 0th character in the subjectText string
756            displayedStringBuilder.setSpan(ForegroundColorSpan.wrap(
757                    isUnread ? sSnippetTextUnreadSpan : sSnippetTextReadSpan), startOffset,
758                    displayedStringBuilder.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
759        }
760        if (isActivated() && showActivatedText()) {
761            displayedStringBuilder.setSpan(sActivatedTextSpan, 0, displayedStringBuilder.length(),
762                    Spannable.SPAN_INCLUSIVE_INCLUSIVE);
763        }
764
765        final int subjectWidth = mCoordinates.subjectWidth;
766        final int subjectHeight = mCoordinates.subjectHeight;
767        mSubjectTextView.setLayoutParams(new ViewGroup.LayoutParams(subjectWidth, subjectHeight));
768        mSubjectTextView.setMaxLines(mCoordinates.subjectLineCount);
769        mSubjectTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCoordinates.subjectFontSize);
770        layoutViewExactly(mSubjectTextView, subjectWidth, subjectHeight);
771
772        mSubjectTextView.setText(displayedStringBuilder);
773    }
774
775    /**
776     * Returns the resource for the text color depending on whether the element is activated or not.
777     * @param defaultColor
778     */
779    private int getFontColor(int defaultColor) {
780        final boolean isBackGroundBlue = isActivated() && showActivatedText();
781        return isBackGroundBlue ? sActivatedTextColor : defaultColor;
782    }
783
784    private boolean showActivatedText() {
785        // For activated elements in tablet in conversation mode, we show an activated color, since
786        // the background is dark blue for activated versus gray for non-activated.
787        final boolean isListCollapsed = mContext.getResources().getBoolean(R.bool.list_collapsed);
788        return mTabletDevice && !isListCollapsed;
789    }
790
791    private boolean canFitFragment(int width, int line, int fixedWidth) {
792        if (line == mCoordinates.sendersLineCount) {
793            return width + fixedWidth <= mSendersWidth;
794        } else {
795            return width <= mSendersWidth;
796        }
797    }
798
799    private void calculateCoordinates() {
800        startTimer(PERF_TAG_CALCULATE_COORDINATES);
801
802        sPaint.setTextSize(mCoordinates.dateFontSize);
803        sPaint.setTypeface(Typeface.DEFAULT);
804        mDateX = mCoordinates.dateXEnd - (int) sPaint.measureText(
805                mHeader.dateText != null ? mHeader.dateText.toString() : "");
806
807        mPaperclipX = mDateX - ATTACHMENT.getWidth() - mCoordinates.datePaddingLeft;
808
809        if (mConfig.isWide()) {
810            // In wide mode, the end of the senders should align with
811            // the start of the subject and is based on a max width.
812            mSendersWidth = mCoordinates.sendersWidth;
813        } else {
814            // In normal mode, the width is based on where the date/attachment icon start.
815            final int dateAttachmentStart;
816            // Have this end near the paperclip or date, not the folders.
817            if (mHeader.paperclip != null) {
818                dateAttachmentStart = mPaperclipX - mCoordinates.paperclipPaddingLeft;
819            } else {
820                dateAttachmentStart = mDateX - mCoordinates.datePaddingLeft;
821            }
822            mSendersWidth = dateAttachmentStart - mCoordinates.sendersX;
823        }
824
825        // Second pass to layout each fragment.
826        int sendersY = mCoordinates.sendersY - mCoordinates.sendersAscent;
827
828        if (mHeader.styledSenders != null) {
829            ellipsizeStyledSenders();
830            layoutSenders();
831        } else {
832            // First pass to calculate width of each fragment.
833            int totalWidth = 0;
834            int fixedWidth = 0;
835            sPaint.setTextSize(mCoordinates.sendersFontSize);
836            sPaint.setTypeface(Typeface.DEFAULT);
837            for (SenderFragment senderFragment : mHeader.senderFragments) {
838                CharacterStyle style = senderFragment.style;
839                int start = senderFragment.start;
840                int end = senderFragment.end;
841                style.updateDrawState(sPaint);
842                senderFragment.width = (int) sPaint.measureText(mHeader.sendersText, start, end);
843                boolean isFixed = senderFragment.isFixed;
844                if (isFixed) {
845                    fixedWidth += senderFragment.width;
846                }
847                totalWidth += senderFragment.width;
848            }
849
850            if (!ConversationItemViewCoordinates.displaySendersInline(mMode)) {
851                sendersY += totalWidth <= mSendersWidth ? mCoordinates.sendersLineHeight / 2 : 0;
852            }
853            if (mSendersWidth < 0) {
854                mSendersWidth = 0;
855            }
856            totalWidth = ellipsize(fixedWidth);
857            mHeader.sendersDisplayLayout = new StaticLayout(mHeader.sendersDisplayText, sPaint,
858                    mSendersWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
859        }
860
861        sPaint.setTextSize(mCoordinates.sendersFontSize);
862        sPaint.setTypeface(Typeface.DEFAULT);
863        if (mSendersWidth < 0) {
864            mSendersWidth = 0;
865        }
866
867        pauseTimer(PERF_TAG_CALCULATE_COORDINATES);
868    }
869
870    // The rules for displaying ellipsized senders are as follows:
871    // 1) If there is message info (either a COUNT or DRAFT info to display), it MUST be shown
872    // 2) If senders do not fit, ellipsize the last one that does fit, and stop
873    // appending new senders
874    private int ellipsizeStyledSenders() {
875        SpannableStringBuilder builder = new SpannableStringBuilder();
876        float totalWidth = 0;
877        boolean ellipsize = false;
878        float width;
879        SpannableStringBuilder messageInfoString =  mHeader.messageInfoString;
880        if (messageInfoString.length() > 0) {
881            CharacterStyle[] spans = messageInfoString.getSpans(0, messageInfoString.length(),
882                    CharacterStyle.class);
883            // There is only 1 character style span; make sure we apply all the
884            // styles to the paint object before measuring.
885            if (spans.length > 0) {
886                spans[0].updateDrawState(sPaint);
887            }
888            // Paint the message info string to see if we lose space.
889            float messageInfoWidth = sPaint.measureText(messageInfoString.toString());
890            totalWidth += messageInfoWidth;
891        }
892       SpannableString prevSender = null;
893       SpannableString ellipsizedText;
894        for (SpannableString sender : mHeader.styledSenders) {
895            // There may be null sender strings if there were dupes we had to remove.
896            if (sender == null) {
897                continue;
898            }
899            // No more width available, we'll only show fixed fragments.
900            if (ellipsize) {
901                break;
902            }
903            CharacterStyle[] spans = sender.getSpans(0, sender.length(), CharacterStyle.class);
904            // There is only 1 character style span.
905            if (spans.length > 0) {
906                spans[0].updateDrawState(sPaint);
907            }
908            // If there are already senders present in this string, we need to
909            // make sure we prepend the dividing token
910            if (SendersView.sElidedString.equals(sender.toString())) {
911                prevSender = sender;
912                sender = copyStyles(spans, sElidedPaddingToken + sender + sElidedPaddingToken);
913            } else if (builder.length() > 0
914                    && (prevSender == null || !SendersView.sElidedString.equals(prevSender
915                            .toString()))) {
916                prevSender = sender;
917                sender = copyStyles(spans, sSendersSplitToken + sender);
918            } else {
919                prevSender = sender;
920            }
921            if (spans.length > 0) {
922                spans[0].updateDrawState(sPaint);
923            }
924            // Measure the width of the current sender and make sure we have space
925            width = (int) sPaint.measureText(sender.toString());
926            if (width + totalWidth > mSendersWidth) {
927                // The text is too long, new line won't help. We have to
928                // ellipsize text.
929                ellipsize = true;
930                width = mSendersWidth - totalWidth; // ellipsis width?
931                ellipsizedText = copyStyles(spans,
932                        TextUtils.ellipsize(sender, sPaint, width, TruncateAt.END));
933                width = (int) sPaint.measureText(ellipsizedText.toString());
934            } else {
935                ellipsizedText = null;
936            }
937            totalWidth += width;
938
939            final CharSequence fragmentDisplayText;
940            if (ellipsizedText != null) {
941                fragmentDisplayText = ellipsizedText;
942            } else {
943                fragmentDisplayText = sender;
944            }
945            builder.append(fragmentDisplayText);
946        }
947        mHeader.styledMessageInfoStringOffset = builder.length();
948        if (messageInfoString != null) {
949            builder.append(messageInfoString);
950        }
951        mHeader.styledSendersString = builder;
952        return (int)totalWidth;
953    }
954
955    private static SpannableString copyStyles(CharacterStyle[] spans, CharSequence newText) {
956        SpannableString s = new SpannableString(newText);
957        if (spans != null && spans.length > 0) {
958            s.setSpan(spans[0], 0, s.length(), 0);
959        }
960        return s;
961    }
962
963    private int ellipsize(int fixedWidth) {
964        int totalWidth = 0;
965        int currentLine = 1;
966        boolean ellipsize = false;
967        for (SenderFragment senderFragment : mHeader.senderFragments) {
968            CharacterStyle style = senderFragment.style;
969            int start = senderFragment.start;
970            int end = senderFragment.end;
971            int width = senderFragment.width;
972            boolean isFixed = senderFragment.isFixed;
973            style.updateDrawState(sPaint);
974
975            // No more width available, we'll only show fixed fragments.
976            if (ellipsize && !isFixed) {
977                senderFragment.shouldDisplay = false;
978                continue;
979            }
980
981            // New line and ellipsize text if needed.
982            senderFragment.ellipsizedText = null;
983            if (isFixed) {
984                fixedWidth -= width;
985            }
986            if (!canFitFragment(totalWidth + width, currentLine, fixedWidth)) {
987                // The text is too long, new line won't help. We have to
988                // ellipsize text.
989                if (totalWidth == 0) {
990                    ellipsize = true;
991                } else {
992                    // New line.
993                    if (currentLine < mCoordinates.sendersLineCount) {
994                        currentLine++;
995                        totalWidth = 0;
996                        // The text is still too long, we have to ellipsize
997                        // text.
998                        if (totalWidth + width > mSendersWidth) {
999                            ellipsize = true;
1000                        }
1001                    } else {
1002                        ellipsize = true;
1003                    }
1004                }
1005
1006                if (ellipsize) {
1007                    width = mSendersWidth - totalWidth;
1008                    // No more new line, we have to reserve width for fixed
1009                    // fragments.
1010                    if (currentLine == mCoordinates.sendersLineCount) {
1011                        width -= fixedWidth;
1012                    }
1013                    senderFragment.ellipsizedText = TextUtils.ellipsize(
1014                            mHeader.sendersText.substring(start, end), sPaint, width,
1015                            TruncateAt.END).toString();
1016                    width = (int) sPaint.measureText(senderFragment.ellipsizedText);
1017                }
1018            }
1019            senderFragment.shouldDisplay = true;
1020            totalWidth += width;
1021
1022            final CharSequence fragmentDisplayText;
1023            if (senderFragment.ellipsizedText != null) {
1024                fragmentDisplayText = senderFragment.ellipsizedText;
1025            } else {
1026                fragmentDisplayText = mHeader.sendersText.substring(start, end);
1027            }
1028            final int spanStart = mHeader.sendersDisplayText.length();
1029            mHeader.sendersDisplayText.append(fragmentDisplayText);
1030            mHeader.sendersDisplayText.setSpan(senderFragment.style, spanStart,
1031                    mHeader.sendersDisplayText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1032        }
1033        return totalWidth;
1034    }
1035
1036    /**
1037     * If the subject contains the tag of a mailing-list (text surrounded with
1038     * []), return the subject with that tag ellipsized, e.g.
1039     * "[android-gmail-team] Hello" -> "[andr...] Hello"
1040     */
1041    private String filterTag(String subject) {
1042        String result = subject;
1043        String formatString = getContext().getResources().getString(R.string.filtered_tag);
1044        if (!TextUtils.isEmpty(subject) && subject.charAt(0) == '[') {
1045            int end = subject.indexOf(']');
1046            if (end > 0) {
1047                String tag = subject.substring(1, end);
1048                result = String.format(formatString, Utils.ellipsize(tag, 7),
1049                        subject.substring(end + 1));
1050            }
1051        }
1052        return result;
1053    }
1054
1055    @Override
1056    protected void onDraw(Canvas canvas) {
1057        // Check mark.
1058        if (mConvListPhotosEnabled) {
1059            canvas.save();
1060            drawContactImages(canvas);
1061            canvas.restore();
1062        } else if (mHeader.checkboxVisible) {
1063            Bitmap checkmark = mChecked ? CHECKMARK_ON : CHECKMARK_OFF;
1064            canvas.drawBitmap(checkmark, mCoordinates.checkmarkX, mCoordinates.checkmarkY, sPaint);
1065        }
1066
1067        // Senders.
1068        boolean isUnread = mHeader.unread;
1069        // Old style senders; apply text colors/ sizes/ styling.
1070        canvas.save();
1071        if (mHeader.sendersDisplayLayout != null) {
1072            sPaint.setTextSize(mCoordinates.sendersFontSize);
1073            sPaint.setTypeface(SendersView.getTypeface(isUnread));
1074            sPaint.setColor(getFontColor(isUnread ?
1075                    sSendersTextColorUnread : sSendersTextColorRead));
1076            canvas.translate(mCoordinates.sendersX, mCoordinates.sendersY
1077                    + mHeader.sendersDisplayLayout.getTopPadding());
1078            mHeader.sendersDisplayLayout.draw(canvas);
1079        } else {
1080            drawSenders(canvas);
1081        }
1082        canvas.restore();
1083
1084
1085        // Subject.
1086        sPaint.setTypeface(Typeface.DEFAULT);
1087        canvas.save();
1088        drawSubject(canvas);
1089        canvas.restore();
1090
1091        // Folders.
1092        if (mConfig.areFoldersVisible()) {
1093            mHeader.folderDisplayer.drawFolders(canvas, mCoordinates, mMode);
1094        }
1095
1096        // If this folder has a color (combined view/Email), show it here
1097        if (mConfig.isColorBlockVisible()) {
1098            sFoldersPaint.setColor(mHeader.conversation.color);
1099            sFoldersPaint.setStyle(Paint.Style.FILL);
1100            canvas.drawRect(mCoordinates.colorBlockX, mCoordinates.colorBlockY,
1101                    mCoordinates.colorBlockX + mCoordinates.colorBlockWidth,
1102                    mCoordinates.colorBlockY + mCoordinates.colorBlockHeight, sFoldersPaint);
1103        }
1104
1105        // Draw the reply state. Draw nothing if neither replied nor forwarded.
1106        if (mConfig.isReplyStateVisible()) {
1107            if (mHeader.hasBeenRepliedTo && mHeader.hasBeenForwarded) {
1108                canvas.drawBitmap(STATE_REPLIED_AND_FORWARDED, mCoordinates.replyStateX,
1109                        mCoordinates.replyStateY, null);
1110            } else if (mHeader.hasBeenRepliedTo) {
1111                canvas.drawBitmap(STATE_REPLIED, mCoordinates.replyStateX,
1112                        mCoordinates.replyStateY, null);
1113            } else if (mHeader.hasBeenForwarded) {
1114                canvas.drawBitmap(STATE_FORWARDED, mCoordinates.replyStateX,
1115                        mCoordinates.replyStateY, null);
1116            } else if (mHeader.isInvite) {
1117                canvas.drawBitmap(STATE_CALENDAR_INVITE, mCoordinates.replyStateX,
1118                        mCoordinates.replyStateY, null);
1119            }
1120        }
1121
1122        // Date.
1123        sPaint.setTextSize(mCoordinates.dateFontSize);
1124        sPaint.setTypeface(Typeface.DEFAULT);
1125        sPaint.setColor(sDateTextColor);
1126        drawText(canvas, mHeader.dateText, mDateX, mCoordinates.dateYBaseline,
1127                sPaint);
1128
1129        // Paper clip icon.
1130        if (mHeader.paperclip != null) {
1131            canvas.drawBitmap(mHeader.paperclip, mPaperclipX, mCoordinates.paperclipY, sPaint);
1132        }
1133
1134        if (mStarEnabled) {
1135            // Star.
1136            canvas.drawBitmap(getStarBitmap(), mCoordinates.starX, mCoordinates.starY, sPaint);
1137        }
1138
1139        // Attachment previews
1140        if (mAttachmentPreviewMode != ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_NONE) {
1141            canvas.save();
1142            drawAttachmentPreviews(canvas);
1143            canvas.restore();
1144        }
1145    }
1146
1147    private void drawContactImages(Canvas canvas) {
1148        canvas.translate(mCoordinates.contactImagesX, mCoordinates.contactImagesY);
1149        mContactImagesHolder.draw(canvas);
1150    }
1151
1152    private void drawAttachmentPreviews(Canvas canvas) {
1153        canvas.translate(mCoordinates.attachmentPreviewsX, mCoordinates.attachmentPreviewsY);
1154        mAttachmentPreviewsCanvas.draw(canvas);
1155    }
1156
1157    private void drawSubject(Canvas canvas) {
1158        canvas.translate(mCoordinates.subjectX, mCoordinates.subjectY);
1159        mSubjectTextView.draw(canvas);
1160    }
1161
1162    private void drawSenders(Canvas canvas) {
1163        canvas.translate(mCoordinates.sendersX, mCoordinates.sendersY);
1164        mSendersTextView.draw(canvas);
1165    }
1166
1167    private Bitmap getStarBitmap() {
1168        return mHeader.conversation.starred ? STAR_ON : STAR_OFF;
1169    }
1170
1171    private static void drawText(Canvas canvas, CharSequence s, int x, int y, TextPaint paint) {
1172        canvas.drawText(s, 0, s.length(), x, y, paint);
1173    }
1174
1175    /**
1176     * Set the background for this item based on:
1177     * 1. Read / Unread (unread messages have a lighter background)
1178     * 2. Tablet / Phone
1179     * 3. Checkbox checked / Unchecked (controls CAB color for item)
1180     * 4. Activated / Not activated (controls the blue highlight on tablet)
1181     * @param isUnread
1182     */
1183    private void updateBackground(boolean isUnread) {
1184        final boolean isListOnTablet = mTabletDevice && mActivity.getViewMode().isListMode();
1185        final int background;
1186        if (isUnread) {
1187            if (isListOnTablet) {
1188                if (mChecked) {
1189                    background = R.drawable.list_conversation_wide_unread_selected_holo;
1190                } else {
1191                    background = R.drawable.conversation_wide_unread_selector;
1192                }
1193            } else {
1194                if (mChecked) {
1195                    background = getCheckedActivatedBackground();
1196                } else {
1197                    background = R.drawable.conversation_unread_selector;
1198                }
1199            }
1200        } else {
1201            if (isListOnTablet) {
1202                if (mChecked) {
1203                    background = R.drawable.list_conversation_wide_read_selected_holo;
1204                } else {
1205                    background = R.drawable.conversation_wide_read_selector;
1206                }
1207            } else {
1208                if (mChecked) {
1209                    background = getCheckedActivatedBackground();
1210                } else {
1211                    background = R.drawable.conversation_read_selector;
1212                }
1213            }
1214        }
1215        setBackgroundResource(background);
1216    }
1217
1218    private final int getCheckedActivatedBackground() {
1219        if (isActivated() && mTabletDevice) {
1220            return R.drawable.list_arrow_selected_holo;
1221        } else {
1222            return R.drawable.list_selected_holo;
1223        }
1224    }
1225
1226    /**
1227     * Toggle the check mark on this view and update the conversation or begin
1228     * drag, if drag is enabled.
1229     */
1230    @Override
1231    public void toggleCheckMarkOrBeginDrag() {
1232        ViewMode mode = mActivity.getViewMode();
1233        if (!mTabletDevice || !mode.isListMode()) {
1234            toggleCheckMark();
1235        } else {
1236            beginDragMode();
1237        }
1238    }
1239
1240    private void toggleCheckMark() {
1241        if (mHeader != null && mHeader.conversation != null) {
1242            mChecked = !mChecked;
1243            Conversation conv = mHeader.conversation;
1244            // Set the list position of this item in the conversation
1245            SwipeableListView listView = getListView();
1246            conv.position = mChecked && listView != null ? listView.getPositionForView(this)
1247                    : Conversation.NO_POSITION;
1248            if (mSelectedConversationSet != null) {
1249                mSelectedConversationSet.toggle(this, conv);
1250            }
1251            if (mSelectedConversationSet.isEmpty()) {
1252                listView.commitDestructiveActions(true);
1253            }
1254            // We update the background after the checked state has changed
1255            // now that we have a selected background asset. Setting the background
1256            // usually waits for a layout pass, but we don't need a full layout,
1257            // just an update to the background.
1258            requestLayout();
1259        }
1260    }
1261
1262    /**
1263     * Toggle the star on this view and update the conversation.
1264     */
1265    public void toggleStar() {
1266        mHeader.conversation.starred = !mHeader.conversation.starred;
1267        Bitmap starBitmap = getStarBitmap();
1268        postInvalidate(mCoordinates.starX, mCoordinates.starY, mCoordinates.starX
1269                + starBitmap.getWidth(),
1270                mCoordinates.starY + starBitmap.getHeight());
1271        ConversationCursor cursor = (ConversationCursor) mAdapter.getCursor();
1272        if (cursor != null) {
1273            cursor.updateBoolean(mHeader.conversation, ConversationColumns.STARRED,
1274                    mHeader.conversation.starred);
1275        }
1276    }
1277
1278    private boolean isTouchInCheckmark(float x, float y) {
1279        // Everything before senders and include a touch slop.
1280        return mHeader.checkboxVisible && x < mCoordinates.sendersX + sTouchSlop;
1281    }
1282
1283    private boolean isTouchInStar(float x, float y) {
1284        // Everything after the star and include a touch slop.
1285        return mStarEnabled && x > mCoordinates.starX - sTouchSlop;
1286    }
1287
1288    @Override
1289    public boolean canChildBeDismissed() {
1290        return true;
1291    }
1292
1293    @Override
1294    public void dismiss() {
1295        SwipeableListView listView = getListView();
1296        if (listView != null) {
1297            getListView().dismissChild(this);
1298        }
1299    }
1300
1301    private boolean onTouchEventNoSwipe(MotionEvent event) {
1302        boolean handled = false;
1303
1304        int x = (int) event.getX();
1305        int y = (int) event.getY();
1306        mLastTouchX = x;
1307        mLastTouchY = y;
1308        switch (event.getAction()) {
1309            case MotionEvent.ACTION_DOWN:
1310                if (isTouchInCheckmark(x, y) || isTouchInStar(x, y)) {
1311                    mDownEvent = true;
1312                    handled = true;
1313                }
1314                break;
1315
1316            case MotionEvent.ACTION_CANCEL:
1317                mDownEvent = false;
1318                break;
1319
1320            case MotionEvent.ACTION_UP:
1321                if (mDownEvent) {
1322                    if (isTouchInCheckmark(x, y)) {
1323                        // Touch on the check mark
1324                        toggleCheckMark();
1325                    } else if (isTouchInStar(x, y)) {
1326                        // Touch on the star
1327                        toggleStar();
1328                    }
1329                    handled = true;
1330                }
1331                break;
1332        }
1333
1334        if (!handled) {
1335            handled = super.onTouchEvent(event);
1336        }
1337
1338        return handled;
1339    }
1340
1341    /**
1342     * ConversationItemView is given the first chance to handle touch events.
1343     */
1344    @Override
1345    public boolean onTouchEvent(MotionEvent event) {
1346        int x = (int) event.getX();
1347        int y = (int) event.getY();
1348        mLastTouchX = x;
1349        mLastTouchY = y;
1350        if (!mSwipeEnabled) {
1351            return onTouchEventNoSwipe(event);
1352        }
1353        switch (event.getAction()) {
1354            case MotionEvent.ACTION_DOWN:
1355                if (isTouchInCheckmark(x, y) || isTouchInStar(x, y)) {
1356                    mDownEvent = true;
1357                    return true;
1358                }
1359                break;
1360            case MotionEvent.ACTION_UP:
1361                if (mDownEvent) {
1362                    if (isTouchInCheckmark(x, y)) {
1363                        // Touch on the check mark
1364                        mDownEvent = false;
1365                        toggleCheckMark();
1366                        return true;
1367                    } else if (isTouchInStar(x, y)) {
1368                        // Touch on the star
1369                        mDownEvent = false;
1370                        toggleStar();
1371                        return true;
1372                    }
1373                }
1374                break;
1375        }
1376        // Let View try to handle it as well.
1377        boolean handled = super.onTouchEvent(event);
1378        if (event.getAction() == MotionEvent.ACTION_DOWN) {
1379            return true;
1380        }
1381        return handled;
1382    }
1383
1384    @Override
1385    public boolean performClick() {
1386        boolean handled = super.performClick();
1387        SwipeableListView list = getListView();
1388        if (list != null && list.getAdapter() != null) {
1389            int pos = list.findConversation(this, mHeader.conversation);
1390            list.performItemClick(this, pos, mHeader.conversation.id);
1391        }
1392        return handled;
1393    }
1394
1395    private SwipeableListView getListView() {
1396        SwipeableListView v = (SwipeableListView) ((SwipeableConversationItemView) getParent())
1397                .getListView();
1398        if (v == null) {
1399            v = mAdapter.getListView();
1400        }
1401        return v;
1402    }
1403
1404    /**
1405     * Reset any state associated with this conversation item view so that it
1406     * can be reused.
1407     */
1408    public void reset() {
1409        setAlpha(1f);
1410        setTranslationX(0f);
1411        mAnimatedHeightFraction = 1.0f;
1412    }
1413
1414    @SuppressWarnings("deprecation")
1415    @Override
1416    public void setTranslationX(float translationX) {
1417        super.setTranslationX(translationX);
1418
1419        final ViewParent vp = getParent();
1420        if (vp == null || !(vp instanceof SwipeableConversationItemView)) {
1421            LogUtils.w(LOG_TAG,
1422                    "CIV.setTranslationX unexpected ConversationItemView parent: %s x=%s",
1423                    vp, translationX);
1424        }
1425
1426        // When a list item is being swiped or animated, ensure that the hosting view has a
1427        // background color set. We only enable the background during the X-translation effect to
1428        // reduce overdraw during normal list scrolling.
1429        final SwipeableConversationItemView parent = (SwipeableConversationItemView) vp;
1430        if (translationX != 0f) {
1431            parent.setBackgroundResource(R.color.swiped_bg_color);
1432        } else {
1433            parent.setBackgroundDrawable(null);
1434        }
1435    }
1436
1437    /**
1438     * Grow the height of the item and fade it in when bringing a conversation
1439     * back from a destructive action.
1440     */
1441    public Animator createSwipeUndoAnimation() {
1442        ObjectAnimator undoAnimator = createTranslateXAnimation(true);
1443        return undoAnimator;
1444    }
1445
1446    /**
1447     * Grow the height of the item and fade it in when bringing a conversation
1448     * back from a destructive action.
1449     */
1450    public Animator createUndoAnimation() {
1451        ObjectAnimator height = createHeightAnimation(true);
1452        Animator fade = ObjectAnimator.ofFloat(this, "alpha", 0, 1.0f);
1453        fade.setDuration(sShrinkAnimationDuration);
1454        fade.setInterpolator(new DecelerateInterpolator(2.0f));
1455        AnimatorSet transitionSet = new AnimatorSet();
1456        transitionSet.playTogether(height, fade);
1457        transitionSet.addListener(new HardwareLayerEnabler(this));
1458        return transitionSet;
1459    }
1460
1461    /**
1462     * Grow the height of the item and fade it in when bringing a conversation
1463     * back from a destructive action.
1464     */
1465    public Animator createDestroyWithSwipeAnimation() {
1466        ObjectAnimator slide = createTranslateXAnimation(false);
1467        ObjectAnimator height = createHeightAnimation(false);
1468        AnimatorSet transitionSet = new AnimatorSet();
1469        transitionSet.playSequentially(slide, height);
1470        return transitionSet;
1471    }
1472
1473    private ObjectAnimator createTranslateXAnimation(boolean show) {
1474        SwipeableListView parent = getListView();
1475        // If we can't get the parent...we have bigger problems.
1476        int width = parent != null ? parent.getMeasuredWidth() : 0;
1477        final float start = show ? width : 0f;
1478        final float end = show ? 0f : width;
1479        ObjectAnimator slide = ObjectAnimator.ofFloat(this, "translationX", start, end);
1480        slide.setInterpolator(new DecelerateInterpolator(2.0f));
1481        slide.setDuration(sSlideAnimationDuration);
1482        return slide;
1483    }
1484
1485    public Animator createDestroyAnimation() {
1486        return createHeightAnimation(false);
1487    }
1488
1489    private ObjectAnimator createHeightAnimation(boolean show) {
1490        final float start = show ? 0f : 1.0f;
1491        final float end = show ? 1.0f : 0f;
1492        ObjectAnimator height = ObjectAnimator.ofFloat(this, "animatedHeightFraction", start, end);
1493        height.setInterpolator(new DecelerateInterpolator(2.0f));
1494        height.setDuration(sShrinkAnimationDuration);
1495        return height;
1496    }
1497
1498    // Used by animator
1499    public void setAnimatedHeightFraction(float height) {
1500        mAnimatedHeightFraction = height;
1501        requestLayout();
1502    }
1503
1504    @Override
1505    public View getSwipeableView() {
1506        return this;
1507    }
1508
1509    /**
1510     * Begin drag mode. Keep the conversation selected (NOT toggle selection) and start drag.
1511     */
1512    private void beginDragMode() {
1513        if (mLastTouchX < 0 || mLastTouchY < 0) {
1514            return;
1515        }
1516        // If this is already checked, don't bother unchecking it!
1517        if (!mChecked) {
1518            toggleCheckMark();
1519        }
1520
1521        // Clip data has form: [conversations_uri, conversationId1,
1522        // maxMessageId1, label1, conversationId2, maxMessageId2, label2, ...]
1523        final int count = mSelectedConversationSet.size();
1524        String description = Utils.formatPlural(mContext, R.plurals.move_conversation, count);
1525
1526        final ClipData data = ClipData.newUri(mContext.getContentResolver(), description,
1527                Conversation.MOVE_CONVERSATIONS_URI);
1528        for (Conversation conversation : mSelectedConversationSet.values()) {
1529            data.addItem(new Item(String.valueOf(conversation.position)));
1530        }
1531        // Protect against non-existent views: only happens for monkeys
1532        final int width = this.getWidth();
1533        final int height = this.getHeight();
1534        final boolean isDimensionNegative = (width < 0) || (height < 0);
1535        if (isDimensionNegative) {
1536            LogUtils.e(LOG_TAG, "ConversationItemView: dimension is negative: "
1537                        + "width=%d, height=%d", width, height);
1538            return;
1539        }
1540        mActivity.startDragMode();
1541        // Start drag mode
1542        startDrag(data, new ShadowBuilder(this, count, mLastTouchX, mLastTouchY), null, 0);
1543    }
1544
1545    /**
1546     * Handles the drag event.
1547     *
1548     * @param event the drag event to be handled
1549     */
1550    @Override
1551    public boolean onDragEvent(DragEvent event) {
1552        switch (event.getAction()) {
1553            case DragEvent.ACTION_DRAG_ENDED:
1554                mActivity.stopDragMode();
1555                return true;
1556        }
1557        return false;
1558    }
1559
1560    private class ShadowBuilder extends DragShadowBuilder {
1561        private final Drawable mBackground;
1562
1563        private final View mView;
1564        private final String mDragDesc;
1565        private final int mTouchX;
1566        private final int mTouchY;
1567        private int mDragDescX;
1568        private int mDragDescY;
1569
1570        public ShadowBuilder(View view, int count, int touchX, int touchY) {
1571            super(view);
1572            mView = view;
1573            mBackground = mView.getResources().getDrawable(R.drawable.list_pressed_holo);
1574            mDragDesc = Utils.formatPlural(mView.getContext(), R.plurals.move_conversation, count);
1575            mTouchX = touchX;
1576            mTouchY = touchY;
1577        }
1578
1579        @Override
1580        public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint) {
1581            int width = mView.getWidth();
1582            int height = mView.getHeight();
1583            mDragDescX = mCoordinates.sendersX;
1584            mDragDescY = getPadding(height, (int) mCoordinates.subjectFontSize)
1585                    - mCoordinates.subjectAscent;
1586            shadowSize.set(width, height);
1587            shadowTouchPoint.set(mTouchX, mTouchY);
1588        }
1589
1590        @Override
1591        public void onDrawShadow(Canvas canvas) {
1592            mBackground.setBounds(0, 0, mView.getWidth(), mView.getHeight());
1593            mBackground.draw(canvas);
1594            sPaint.setTextSize(mCoordinates.subjectFontSize);
1595            canvas.drawText(mDragDesc, mDragDescX, mDragDescY, sPaint);
1596        }
1597    }
1598
1599    @Override
1600    public float getMinAllowScrollDistance() {
1601        return sScrollSlop;
1602    }
1603}
1604