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