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