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