ConversationItemView.java revision bc9b18ceda6603d9337e2410429ac49cd60c0345
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            totalWidth = ellipsize(fixedWidth, sendersY);
777            mHeader.sendersDisplayLayout = new StaticLayout(mHeader.sendersDisplayText, sPaint,
778                    mSendersWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
779        }
780
781        sPaint.setTextSize(mCoordinates.sendersFontSize);
782        sPaint.setTypeface(Typeface.DEFAULT);
783        if (mSendersWidth < 0) {
784            mSendersWidth = 0;
785        }
786
787        pauseTimer(PERF_TAG_CALCULATE_COORDINATES);
788    }
789
790    // The rules for displaying ellipsized senders are as follows:
791    // 1) If there is message info (either a COUNT or DRAFT info to display), it MUST be shown
792    // 2) If senders do not fit, ellipsize the last one that does fit, and stop
793    // appending new senders
794    private int ellipsizeStyledSenders() {
795        SpannableStringBuilder builder = new SpannableStringBuilder();
796        float totalWidth = 0;
797        boolean ellipsize = false;
798        float width;
799        SpannableStringBuilder messageInfoString =  mHeader.messageInfoString;
800        if (messageInfoString.length() > 0) {
801            CharacterStyle[] spans = messageInfoString.getSpans(0, messageInfoString.length(),
802                    CharacterStyle.class);
803            // There is only 1 character style span; make sure we apply all the
804            // styles to the paint object before measuring.
805            if (spans.length > 0) {
806                spans[0].updateDrawState(sPaint);
807            }
808            // Paint the message info string to see if we lose space.
809            float messageInfoWidth = sPaint.measureText(messageInfoString.toString());
810            totalWidth += messageInfoWidth;
811        }
812       SpannableString prevSender = null;
813       SpannableString ellipsizedText;
814        for (SpannableString sender : mHeader.styledSenders) {
815            // There may be null sender strings if there were dupes we had to remove.
816            if (sender == null) {
817                continue;
818            }
819            // No more width available, we'll only show fixed fragments.
820            if (ellipsize) {
821                break;
822            }
823            CharacterStyle[] spans = sender.getSpans(0, sender.length(), CharacterStyle.class);
824            // There is only 1 character style span.
825            if (spans.length > 0) {
826                spans[0].updateDrawState(sPaint);
827            }
828            // If there are already senders present in this string, we need to
829            // make sure we prepend the dividing token
830            if (SendersView.sElidedString.equals(sender.toString())) {
831                prevSender = sender;
832                sender = copyStyles(spans, sElidedPaddingToken + sender + sElidedPaddingToken);
833            } else if (builder.length() > 0
834                    && (prevSender == null || !SendersView.sElidedString.equals(prevSender
835                            .toString()))) {
836                prevSender = sender;
837                sender = copyStyles(spans, sSendersSplitToken + sender);
838            } else {
839                prevSender = sender;
840            }
841            if (spans.length > 0) {
842                spans[0].updateDrawState(sPaint);
843            }
844            // Measure the width of the current sender and make sure we have space
845            width = (int) sPaint.measureText(sender.toString());
846            if (width + totalWidth > mSendersWidth) {
847                // The text is too long, new line won't help. We have to
848                // ellipsize text.
849                ellipsize = true;
850                width = mSendersWidth - totalWidth; // ellipsis width?
851                ellipsizedText = copyStyles(spans,
852                        TextUtils.ellipsize(sender, sPaint, width, TruncateAt.END));
853                width = (int) sPaint.measureText(ellipsizedText.toString());
854            } else {
855                ellipsizedText = null;
856            }
857            totalWidth += width;
858
859            final CharSequence fragmentDisplayText;
860            if (ellipsizedText != null) {
861                fragmentDisplayText = ellipsizedText;
862            } else {
863                fragmentDisplayText = sender;
864            }
865            builder.append(fragmentDisplayText);
866        }
867        mHeader.styledMessageInfoStringOffset = builder.length();
868        if (messageInfoString != null) {
869            builder.append(messageInfoString);
870        }
871        mHeader.styledSendersString = builder;
872        return (int)totalWidth;
873    }
874
875    private SpannableString copyStyles(CharacterStyle[] spans, CharSequence newText) {
876        SpannableString s = new SpannableString(newText);
877        if (spans != null && spans.length > 0) {
878            s.setSpan(spans[0], 0, s.length(), 0);
879        }
880        return s;
881    }
882
883    private int ellipsize(int fixedWidth, int sendersY) {
884        int totalWidth = 0;
885        int currentLine = 1;
886        boolean ellipsize = false;
887        for (SenderFragment senderFragment : mHeader.senderFragments) {
888            CharacterStyle style = senderFragment.style;
889            int start = senderFragment.start;
890            int end = senderFragment.end;
891            int width = senderFragment.width;
892            boolean isFixed = senderFragment.isFixed;
893            style.updateDrawState(sPaint);
894
895            // No more width available, we'll only show fixed fragments.
896            if (ellipsize && !isFixed) {
897                senderFragment.shouldDisplay = false;
898                continue;
899            }
900
901            // New line and ellipsize text if needed.
902            senderFragment.ellipsizedText = null;
903            if (isFixed) {
904                fixedWidth -= width;
905            }
906            if (!canFitFragment(totalWidth + width, currentLine, fixedWidth)) {
907                // The text is too long, new line won't help. We have to
908                // ellipsize text.
909                if (totalWidth == 0) {
910                    ellipsize = true;
911                } else {
912                    // New line.
913                    if (currentLine < mCoordinates.sendersLineCount) {
914                        currentLine++;
915                        sendersY += mCoordinates.sendersLineHeight;
916                        totalWidth = 0;
917                        // The text is still too long, we have to ellipsize
918                        // text.
919                        if (totalWidth + width > mSendersWidth) {
920                            ellipsize = true;
921                        }
922                    } else {
923                        ellipsize = true;
924                    }
925                }
926
927                if (ellipsize) {
928                    width = mSendersWidth - totalWidth;
929                    // No more new line, we have to reserve width for fixed
930                    // fragments.
931                    if (currentLine == mCoordinates.sendersLineCount) {
932                        width -= fixedWidth;
933                    }
934                    senderFragment.ellipsizedText = TextUtils.ellipsize(
935                            mHeader.sendersText.substring(start, end), sPaint, width,
936                            TruncateAt.END).toString();
937                    width = (int) sPaint.measureText(senderFragment.ellipsizedText);
938                }
939            }
940            senderFragment.shouldDisplay = true;
941            totalWidth += width;
942
943            final CharSequence fragmentDisplayText;
944            if (senderFragment.ellipsizedText != null) {
945                fragmentDisplayText = senderFragment.ellipsizedText;
946            } else {
947                fragmentDisplayText = mHeader.sendersText.substring(start, end);
948            }
949            final int spanStart = mHeader.sendersDisplayText.length();
950            mHeader.sendersDisplayText.append(fragmentDisplayText);
951            mHeader.sendersDisplayText.setSpan(senderFragment.style, spanStart,
952                    mHeader.sendersDisplayText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
953        }
954        return totalWidth;
955    }
956
957    /**
958     * If the subject contains the tag of a mailing-list (text surrounded with
959     * []), return the subject with that tag ellipsized, e.g.
960     * "[android-gmail-team] Hello" -> "[andr...] Hello"
961     */
962    private String filterTag(String subject) {
963        String result = subject;
964        String formatString = getContext().getResources().getString(R.string.filtered_tag);
965        if (!TextUtils.isEmpty(subject) && subject.charAt(0) == '[') {
966            int end = subject.indexOf(']');
967            if (end > 0) {
968                String tag = subject.substring(1, end);
969                result = String.format(formatString, Utils.ellipsize(tag, 7),
970                        subject.substring(end + 1));
971            }
972        }
973        return result;
974    }
975
976    @Override
977    protected void onDraw(Canvas canvas) {
978        // Check mark.
979        if (mHeader.checkboxVisible) {
980            Bitmap checkmark = mChecked ? CHECKMARK_ON : CHECKMARK_OFF;
981            canvas.drawBitmap(checkmark, mCoordinates.checkmarkX, mCoordinates.checkmarkY, sPaint);
982        }
983
984        // Personal Level.
985        if (mCoordinates.showPersonalLevel && mHeader.personalLevelBitmap != null) {
986            canvas.drawBitmap(mHeader.personalLevelBitmap, mCoordinates.personalLevelX,
987                    mCoordinates.personalLevelY, sPaint);
988        }
989
990        // Senders.
991        boolean isUnread = mHeader.unread;
992        // Old style senders; apply text colors/ sizes/ styling.
993        canvas.save();
994        if (mHeader.sendersDisplayLayout != null) {
995            sPaint.setTextSize(mCoordinates.sendersFontSize);
996            sPaint.setTypeface(SendersView.getTypeface(isUnread));
997            sPaint.setColor(getFontColor(isUnread ? sSendersTextColorUnread
998                    : sSendersTextColorRead));
999            canvas.translate(mCoordinates.sendersX,
1000                    mCoordinates.sendersY + mHeader.sendersDisplayLayout.getTopPadding());
1001            mHeader.sendersDisplayLayout.draw(canvas);
1002        } else {
1003            canvas.translate(mCoordinates.sendersX,
1004                    mCoordinates.sendersY + sSendersTextViewTopPadding);
1005            mHeader.sendersTextView.layout(0, 0, mSendersWidth, sSendersTextViewHeight);
1006            mHeader.sendersTextView.draw(canvas);
1007        }
1008        canvas.restore();
1009
1010
1011        // Subject.
1012        sPaint.setTextSize(mCoordinates.subjectFontSize);
1013        sPaint.setTypeface(Typeface.DEFAULT);
1014        canvas.save();
1015        if (isActivated() && showActivatedText()) {
1016            if (mHeader.subjectLayoutActivated != null) {
1017                canvas.translate(mCoordinates.subjectX, mCoordinates.subjectY
1018                        + mHeader.subjectLayoutActivated.getTopPadding());
1019                mHeader.subjectLayoutActivated.draw(canvas);
1020            }
1021        } else if (mHeader.subjectLayout != null) {
1022            canvas.translate(mCoordinates.subjectX,
1023                    mCoordinates.subjectY + mHeader.subjectLayout.getTopPadding());
1024            mHeader.subjectLayout.draw(canvas);
1025        }
1026        canvas.restore();
1027
1028        // Folders.
1029        if (mCoordinates.showFolders) {
1030            mHeader.folderDisplayer.drawFolders(canvas, mCoordinates, mFoldersXEnd, mMode);
1031        }
1032
1033        // If this folder has a color (combined view/Email), show it here
1034        if (mHeader.conversation.color != 0) {
1035            sFoldersPaint.setColor(mHeader.conversation.color);
1036            sFoldersPaint.setStyle(Paint.Style.FILL);
1037            int width = ConversationItemViewCoordinates.getColorBlockWidth(mContext);
1038            int height = ConversationItemViewCoordinates.getColorBlockHeight(mContext);
1039            canvas.drawRect(mCoordinates.dateXEnd - width, 0, mCoordinates.dateXEnd,
1040                    height, sFoldersPaint);
1041        }
1042
1043        // Date background: shown when there is an attachment or a visible
1044        // folder.
1045        if (!isActivated()
1046                && (mHeader.conversation.hasAttachments ||
1047                        (mHeader.folderDisplayer != null
1048                            && mHeader.folderDisplayer.hasVisibleFolders()))
1049                && ConversationItemViewCoordinates.showAttachmentBackground(mMode)) {
1050            int leftOffset = (mHeader.conversation.hasAttachments ? mPaperclipX : mDateX)
1051                    - sDateBackgroundPaddingLeft;
1052            int top = mCoordinates.showFolders ? mCoordinates.foldersY : mCoordinates.dateY;
1053            mHeader.dateBackground = getDateBackground(mHeader.conversation.hasAttachments);
1054            canvas.drawBitmap(mHeader.dateBackground, leftOffset, top, sPaint);
1055        } else {
1056            mHeader.dateBackground = null;
1057        }
1058
1059        // Draw the reply state. Draw nothing if neither replied nor forwarded.
1060        if (mCoordinates.showReplyState) {
1061            if (mHeader.hasBeenRepliedTo && mHeader.hasBeenForwarded) {
1062                canvas.drawBitmap(STATE_REPLIED_AND_FORWARDED, mCoordinates.replyStateX,
1063                        mCoordinates.replyStateY, null);
1064            } else if (mHeader.hasBeenRepliedTo) {
1065                canvas.drawBitmap(STATE_REPLIED, mCoordinates.replyStateX,
1066                        mCoordinates.replyStateY, null);
1067            } else if (mHeader.hasBeenForwarded) {
1068                canvas.drawBitmap(STATE_FORWARDED, mCoordinates.replyStateX,
1069                        mCoordinates.replyStateY, null);
1070            } else if (mHeader.isInvite) {
1071                canvas.drawBitmap(STATE_CALENDAR_INVITE, mCoordinates.replyStateX,
1072                        mCoordinates.replyStateY, null);
1073            }
1074        }
1075
1076        // Date.
1077        sPaint.setTextSize(mCoordinates.dateFontSize);
1078        sPaint.setTypeface(Typeface.DEFAULT);
1079        sPaint.setColor(sDateTextColor);
1080        drawText(canvas, mHeader.dateText, mDateX, mCoordinates.dateY - mCoordinates.dateAscent,
1081                sPaint);
1082
1083        // Paper clip icon.
1084        if (mHeader.paperclip != null) {
1085            canvas.drawBitmap(mHeader.paperclip, mPaperclipX, mCoordinates.paperclipY, sPaint);
1086        }
1087
1088        if (mHeader.faded) {
1089            int fadedColor = -1;
1090            if (sFadedActivatedColor == -1) {
1091                sFadedActivatedColor = mContext.getResources().getColor(
1092                        R.color.faded_activated_conversation_header);
1093            }
1094            fadedColor = sFadedActivatedColor;
1095            int restoreState = canvas.save();
1096            Rect bounds = canvas.getClipBounds();
1097            canvas.clipRect(bounds.left, bounds.top, bounds.right
1098                    - mContext.getResources().getDimensionPixelSize(R.dimen.triangle_width),
1099                    bounds.bottom);
1100            canvas.drawARGB(Color.alpha(fadedColor), Color.red(fadedColor),
1101                    Color.green(fadedColor), Color.blue(fadedColor));
1102            canvas.restoreToCount(restoreState);
1103        }
1104
1105        // Star.
1106        canvas.drawBitmap(getStarBitmap(), mCoordinates.starX, mCoordinates.starY, sPaint);
1107    }
1108
1109    private Bitmap getStarBitmap() {
1110        return mHeader.conversation.starred ? STAR_ON : STAR_OFF;
1111    }
1112
1113    private Bitmap getDateBackground(boolean hasAttachments) {
1114        int leftOffset = (hasAttachments ? mPaperclipX : mDateX) - sDateBackgroundPaddingLeft;
1115        if (hasAttachments) {
1116            if (sDateBackgroundAttachment == null) {
1117                sDateBackgroundAttachment = Bitmap.createScaledBitmap(DATE_BACKGROUND, mViewWidth
1118                        - leftOffset, sDateBackgroundHeight, false);
1119            }
1120            return sDateBackgroundAttachment;
1121        } else {
1122            if (sDateBackgroundNoAttachment == null) {
1123                sDateBackgroundNoAttachment = Bitmap.createScaledBitmap(DATE_BACKGROUND, mViewWidth
1124                        - leftOffset, sDateBackgroundHeight, false);
1125            }
1126            return sDateBackgroundNoAttachment;
1127        }
1128    }
1129
1130    private void drawText(Canvas canvas, CharSequence s, int x, int y, TextPaint paint) {
1131        canvas.drawText(s, 0, s.length(), x, y, paint);
1132    }
1133
1134    private void updateBackground(boolean isUnread) {
1135        if (mBackgroundOverride != -1) {
1136            // If the item is animating, we use a color to avoid shrinking a 9-patch
1137            // and getting weird artifacts from the overlap.
1138            setBackgroundColor(mBackgroundOverride);
1139            return;
1140        }
1141        final boolean isListOnTablet = mTabletDevice && mActivity.getViewMode().isListMode();
1142        if (isUnread) {
1143            if (isListOnTablet) {
1144                if (mChecked) {
1145                    setBackgroundResource(R.drawable.list_conversation_wide_unread_selected_holo);
1146                } else {
1147                    setBackgroundResource(R.drawable.conversation_wide_unread_selector);
1148                }
1149            } else {
1150                if (mChecked) {
1151                    setCheckedActivatedBackground();
1152                } else {
1153                    setBackgroundResource(R.drawable.conversation_unread_selector);
1154                }
1155            }
1156        } else {
1157            if (isListOnTablet) {
1158                if (mChecked) {
1159                    setBackgroundResource(R.drawable.list_conversation_wide_read_selected_holo);
1160                } else {
1161                    setBackgroundResource(R.drawable.conversation_wide_read_selector);
1162                }
1163            } else {
1164                if (mChecked) {
1165                    setCheckedActivatedBackground();
1166                } else {
1167                    setBackgroundResource(R.drawable.conversation_read_selector);
1168                }
1169            }
1170        }
1171    }
1172
1173    private void setCheckedActivatedBackground() {
1174        if (isActivated() && mTabletDevice) {
1175            setBackgroundResource(R.drawable.list_arrow_selected_holo);
1176        } else {
1177            setBackgroundResource(R.drawable.list_selected_holo);
1178        }
1179    }
1180
1181    /**
1182     * Toggle the check mark on this view and update the conversation or begin
1183     * drag, if drag is enabled.
1184     */
1185    public void toggleCheckMarkOrBeginDrag() {
1186        ViewMode mode = mActivity.getViewMode();
1187        if (!mTabletDevice || !mode.isListMode()) {
1188            toggleCheckMark();
1189        } else {
1190            beginDragMode();
1191        }
1192    }
1193
1194    private void toggleCheckMark() {
1195        if (mHeader != null && mHeader.conversation != null) {
1196            mChecked = !mChecked;
1197            Conversation conv = mHeader.conversation;
1198            // Set the list position of this item in the conversation
1199            SwipeableListView listView = getListView();
1200            conv.position = mChecked && listView != null ? listView.getPositionForView(this)
1201                    : Conversation.NO_POSITION;
1202            if (mSelectedConversationSet != null) {
1203                mSelectedConversationSet.toggle(this, conv);
1204            }
1205            if (mSelectedConversationSet.isEmpty()) {
1206                listView.commitDestructiveActions(true);
1207            }
1208            // We update the background after the checked state has changed
1209            // now that we have a selected background asset. Setting the background
1210            // usually waits for a layout pass, but we don't need a full layout,
1211            // just an update to the background.
1212            requestLayout();
1213        }
1214    }
1215
1216    /**
1217     * Return if the checkbox for this item is checked.
1218     */
1219    public boolean isChecked() {
1220        return mChecked;
1221    }
1222
1223    /**
1224     * Toggle the star on this view and update the conversation.
1225     */
1226    public void toggleStar() {
1227        mHeader.conversation.starred = !mHeader.conversation.starred;
1228        Bitmap starBitmap = getStarBitmap();
1229        postInvalidate(mCoordinates.starX, mCoordinates.starY, mCoordinates.starX
1230                + starBitmap.getWidth(),
1231                mCoordinates.starY + starBitmap.getHeight());
1232        ConversationCursor cursor = (ConversationCursor)mAdapter.getCursor();
1233        cursor.updateBoolean(mContext, mHeader.conversation, ConversationColumns.STARRED,
1234                mHeader.conversation.starred);
1235    }
1236
1237    private boolean isTouchInCheckmark(float x, float y) {
1238        // Everything before senders and include a touch slop.
1239        return mHeader.checkboxVisible && x < mCoordinates.sendersX + sTouchSlop;
1240    }
1241
1242    private boolean isTouchInStar(float x, float y) {
1243        // Everything after the star and include a touch slop.
1244        return x > mCoordinates.starX - sTouchSlop;
1245    }
1246
1247    @Override
1248    public boolean canChildBeDismissed() {
1249        return true;
1250    }
1251
1252    @Override
1253    public void dismiss() {
1254        SwipeableListView listView = getListView();
1255        if (listView != null) {
1256            getListView().dismissChild(this);
1257        }
1258    }
1259
1260    private boolean onTouchEventNoSwipe(MotionEvent event) {
1261        boolean handled = false;
1262
1263        int x = (int) event.getX();
1264        int y = (int) event.getY();
1265        mLastTouchX = x;
1266        mLastTouchY = y;
1267        switch (event.getAction()) {
1268            case MotionEvent.ACTION_DOWN:
1269                if (isTouchInCheckmark(x, y) || isTouchInStar(x, y)) {
1270                    mDownEvent = true;
1271                    handled = true;
1272                }
1273                break;
1274
1275            case MotionEvent.ACTION_CANCEL:
1276                mDownEvent = false;
1277                break;
1278
1279            case MotionEvent.ACTION_UP:
1280                if (mDownEvent) {
1281                    if (isTouchInCheckmark(x, y)) {
1282                        // Touch on the check mark
1283                        toggleCheckMark();
1284                    } else if (isTouchInStar(x, y)) {
1285                        // Touch on the star
1286                        toggleStar();
1287                    }
1288                    handled = true;
1289                }
1290                break;
1291        }
1292
1293        if (!handled) {
1294            handled = super.onTouchEvent(event);
1295        }
1296
1297        return handled;
1298    }
1299
1300    /**
1301     * ConversationItemView is given the first chance to handle touch events.
1302     */
1303    @Override
1304    public boolean onTouchEvent(MotionEvent event) {
1305        int x = (int) event.getX();
1306        int y = (int) event.getY();
1307        mLastTouchX = x;
1308        mLastTouchY = y;
1309        if (!mSwipeEnabled) {
1310            return onTouchEventNoSwipe(event);
1311        }
1312        switch (event.getAction()) {
1313            case MotionEvent.ACTION_DOWN:
1314                if (isTouchInCheckmark(x, y) || isTouchInStar(x, y)) {
1315                    mDownEvent = true;
1316                    return true;
1317                }
1318                break;
1319            case MotionEvent.ACTION_UP:
1320                if (mDownEvent) {
1321                    if (isTouchInCheckmark(x, y)) {
1322                        // Touch on the check mark
1323                        mDownEvent = false;
1324                        toggleCheckMark();
1325                        return true;
1326                    } else if (isTouchInStar(x, y)) {
1327                        // Touch on the star
1328                        mDownEvent = false;
1329                        toggleStar();
1330                        return true;
1331                    }
1332                }
1333                break;
1334        }
1335        // Let View try to handle it as well.
1336        boolean handled = super.onTouchEvent(event);
1337        if (event.getAction() == MotionEvent.ACTION_DOWN) {
1338            return true;
1339        }
1340        return handled;
1341    }
1342
1343    @Override
1344    public boolean performClick() {
1345        boolean handled = super.performClick();
1346        SwipeableListView list = getListView();
1347        if (list != null) {
1348            int pos = list.findConversation(this, mHeader.conversation);
1349            list.performItemClick(this, pos, mHeader.conversation.id);
1350        }
1351        return handled;
1352    }
1353
1354    private SwipeableListView getListView() {
1355        SwipeableListView v = (SwipeableListView) ((SwipeableConversationItemView) getParent())
1356                .getListView();
1357        if (v == null) {
1358            v = mAdapter.getListView();
1359        }
1360        return v;
1361    }
1362
1363    /**
1364     * Reset any state associated with this conversation item view so that it
1365     * can be reused.
1366     */
1367    public void reset() {
1368        mBackgroundOverride = -1;
1369        setAlpha(1);
1370        setTranslationX(0);
1371        setAnimatedHeight(-1);
1372        setMinimumHeight(ConversationItemViewCoordinates.getMinHeight(mContext,
1373                mActivity.getViewMode()));
1374    }
1375
1376    /**
1377     * Grow the height of the item and fade it in when bringing a conversation
1378     * back from a destructive action.
1379     * @param listener
1380     */
1381    public void startSwipeUndoAnimation(ViewMode viewMode, final AnimatorListener listener) {
1382        ObjectAnimator undoAnimator = createTranslateXAnimation(true);
1383        undoAnimator.addListener(listener);
1384        undoAnimator.start();
1385    }
1386
1387    /**
1388     * Grow the height of the item and fade it in when bringing a conversation
1389     * back from a destructive action.
1390     * @param listener
1391     */
1392    public void startUndoAnimation(ViewMode viewMode, final AnimatorListener listener) {
1393        int minHeight = ConversationItemViewCoordinates.getMinHeight(mContext, viewMode);
1394        setMinimumHeight(minHeight);
1395        mAnimatedHeight = 0;
1396        ObjectAnimator height = createHeightAnimation(true);
1397        Animator fade = ObjectAnimator.ofFloat(this, "itemAlpha", 0, 1.0f);
1398        fade.setDuration(sShrinkAnimationDuration);
1399        fade.setInterpolator(new DecelerateInterpolator(2.0f));
1400        AnimatorSet transitionSet = new AnimatorSet();
1401        transitionSet.playTogether(height, fade);
1402        transitionSet.addListener(listener);
1403        transitionSet.start();
1404    }
1405
1406    /**
1407     * Grow the height of the item and fade it in when bringing a conversation
1408     * back from a destructive action.
1409     * @param listener
1410     */
1411    public void startDestroyWithSwipeAnimation(final AnimatorListener listener) {
1412        ObjectAnimator slide = createTranslateXAnimation(false);
1413        ObjectAnimator height = createHeightAnimation(false);
1414        AnimatorSet transitionSet = new AnimatorSet();
1415        transitionSet.playSequentially(slide, height);
1416        transitionSet.addListener(listener);
1417        transitionSet.start();
1418    }
1419
1420    private ObjectAnimator createTranslateXAnimation(boolean show) {
1421        SwipeableListView parent = getListView();
1422        // If we can't get the parent...we have bigger problems.
1423        int width = parent != null ? parent.getMeasuredWidth() : 0;
1424        final float start = show ? width : 0f;
1425        final float end = show ? 0f : width;
1426        ObjectAnimator slide = ObjectAnimator.ofFloat(this, "translationX", start, end);
1427        slide.setInterpolator(new DecelerateInterpolator(2.0f));
1428        slide.setDuration(sSlideAnimationDuration);
1429        return slide;
1430    }
1431
1432    public void startDestroyAnimation(final AnimatorListener listener) {
1433        ObjectAnimator height = createHeightAnimation(false);
1434        int minHeight = ConversationItemViewCoordinates.getMinHeight(mContext,
1435                mActivity.getViewMode());
1436        setMinimumHeight(0);
1437        mBackgroundOverride = sAnimatingBackgroundColor;
1438        setBackgroundColor(mBackgroundOverride);
1439        mAnimatedHeight = minHeight;
1440        height.addListener(listener);
1441        height.start();
1442    }
1443
1444    private ObjectAnimator createHeightAnimation(boolean show) {
1445        int minHeight = ConversationItemViewCoordinates.getMinHeight(getContext(),
1446                mActivity.getViewMode());
1447        final int start = show ? 0 : minHeight;
1448        final int end = show ? minHeight : 0;
1449        ObjectAnimator height = ObjectAnimator.ofInt(this, "animatedHeight", start, end);
1450        height.setInterpolator(new DecelerateInterpolator(2.0f));
1451        height.setDuration(sShrinkAnimationDuration);
1452        return height;
1453    }
1454
1455    // Used by animator
1456    @SuppressWarnings("unused")
1457    public void setItemAlpha(float alpha) {
1458        setAlpha(alpha);
1459        invalidate();
1460    }
1461
1462    // Used by animator
1463    @SuppressWarnings("unused")
1464    public void setAnimatedHeight(int height) {
1465        mAnimatedHeight = height;
1466        requestLayout();
1467    }
1468
1469    @Override
1470    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1471        if (mAnimatedHeight == -1) {
1472            int height = measureHeight(heightMeasureSpec,
1473                    ConversationItemViewCoordinates.getMode(mContext, mActivity.getViewMode()));
1474            setMeasuredDimension(widthMeasureSpec, height);
1475        } else {
1476            setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mAnimatedHeight);
1477        }
1478    }
1479
1480    /**
1481     * Determine the height of this view.
1482     * @param measureSpec A measureSpec packed into an int
1483     * @param mode The current mode of this view
1484     * @return The height of the view, honoring constraints from measureSpec
1485     */
1486    private int measureHeight(int measureSpec, int mode) {
1487        int result = 0;
1488        int specMode = MeasureSpec.getMode(measureSpec);
1489        int specSize = MeasureSpec.getSize(measureSpec);
1490
1491        if (specMode == MeasureSpec.EXACTLY) {
1492            // We were told how big to be
1493            result = specSize;
1494        } else {
1495            // Measure the text
1496            result = ConversationItemViewCoordinates.getHeight(mContext, mode);
1497            if (specMode == MeasureSpec.AT_MOST) {
1498                // Respect AT_MOST value if that was what is called for by
1499                // measureSpec
1500                result = Math.min(result, specSize);
1501            }
1502        }
1503        return result;
1504    }
1505
1506    /**
1507     * Get the current position of this conversation item in the list.
1508     */
1509    public int getPosition() {
1510        return mHeader != null && mHeader.conversation != null ?
1511                mHeader.conversation.position : -1;
1512    }
1513
1514    @Override
1515    public View getSwipeableView() {
1516        return this;
1517    }
1518
1519    /**
1520     * Begin drag mode. Keep the conversation selected (NOT toggle selection) and start drag.
1521     */
1522    private void beginDragMode() {
1523        if (mLastTouchX < 0 || mLastTouchY < 0) {
1524            return;
1525        }
1526        // If this is already checked, don't bother unchecking it!
1527        if (!mChecked) {
1528            toggleCheckMark();
1529        }
1530
1531        // Clip data has form: [conversations_uri, conversationId1,
1532        // maxMessageId1, label1, conversationId2, maxMessageId2, label2, ...]
1533        final int count = mSelectedConversationSet.size();
1534        String description = Utils.formatPlural(mContext, R.plurals.move_conversation, count);
1535
1536        final ClipData data = ClipData.newUri(mContext.getContentResolver(), description,
1537                Conversation.MOVE_CONVERSATIONS_URI);
1538        for (Conversation conversation : mSelectedConversationSet.values()) {
1539            data.addItem(new Item(String.valueOf(conversation.position)));
1540        }
1541        // Protect against non-existent views: only happens for monkeys
1542        final int width = this.getWidth();
1543        final int height = this.getHeight();
1544        final boolean isDimensionNegative = (width < 0) || (height < 0);
1545        if (isDimensionNegative) {
1546            LogUtils.e(LOG_TAG, "ConversationItemView: dimension is negative: "
1547                        + "width=%d, height=%d", width, height);
1548            return;
1549        }
1550        mActivity.startDragMode();
1551        // Start drag mode
1552        startDrag(data, new ShadowBuilder(this, count, mLastTouchX, mLastTouchY), null, 0);
1553    }
1554
1555    /**
1556     * Handles the drag event.
1557     *
1558     * @param event the drag event to be handled
1559     */
1560    @Override
1561    public boolean onDragEvent(DragEvent event) {
1562        switch (event.getAction()) {
1563            case DragEvent.ACTION_DRAG_ENDED:
1564                mActivity.stopDragMode();
1565                return true;
1566        }
1567        return false;
1568    }
1569
1570    private class ShadowBuilder extends DragShadowBuilder {
1571        private final Drawable mBackground;
1572
1573        private final View mView;
1574        private final String mDragDesc;
1575        private final int mTouchX;
1576        private final int mTouchY;
1577        private int mDragDescX;
1578        private int mDragDescY;
1579
1580        public ShadowBuilder(View view, int count, int touchX, int touchY) {
1581            super(view);
1582            mView = view;
1583            mBackground = mView.getResources().getDrawable(R.drawable.list_pressed_holo);
1584            mDragDesc = Utils.formatPlural(mView.getContext(), R.plurals.move_conversation, count);
1585            mTouchX = touchX;
1586            mTouchY = touchY;
1587        }
1588
1589        @Override
1590        public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint) {
1591            int width = mView.getWidth();
1592            int height = mView.getHeight();
1593            mDragDescX = mCoordinates.sendersX;
1594            mDragDescY = getPadding(height, mCoordinates.subjectFontSize)
1595                    - mCoordinates.subjectAscent;
1596            shadowSize.set(width, height);
1597            shadowTouchPoint.set(mTouchX, mTouchY);
1598        }
1599
1600        @Override
1601        public void onDrawShadow(Canvas canvas) {
1602            mBackground.setBounds(0, 0, mView.getWidth(), mView.getHeight());
1603            mBackground.draw(canvas);
1604            sPaint.setTextSize(mCoordinates.subjectFontSize);
1605            canvas.drawText(mDragDesc, mDragDescX, mDragDescY, sPaint);
1606        }
1607    }
1608
1609    @Override
1610    public float getMinAllowScrollDistance() {
1611        return sScrollSlop;
1612    }
1613}
1614