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