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