ConversationItemView.java revision e1ba1013409486f88f06af2932546a1b366704d3
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.AnimatorSet;
22import android.animation.ObjectAnimator;
23import android.content.BroadcastReceiver;
24import android.content.ClipData;
25import android.content.ClipData.Item;
26import android.content.Context;
27import android.content.Intent;
28import android.content.IntentFilter;
29import android.content.res.Resources;
30import android.graphics.Bitmap;
31import android.graphics.BitmapFactory;
32import android.graphics.Canvas;
33import android.graphics.Color;
34import android.graphics.LinearGradient;
35import android.graphics.Paint;
36import android.graphics.Point;
37import android.graphics.Rect;
38import android.graphics.Shader;
39import android.graphics.Typeface;
40import android.graphics.drawable.Drawable;
41import android.graphics.drawable.InsetDrawable;
42import android.support.v4.text.BidiFormatter;
43import android.support.v4.view.ViewCompat;
44import android.text.Layout.Alignment;
45import android.text.Spannable;
46import android.text.SpannableString;
47import android.text.SpannableStringBuilder;
48import android.text.StaticLayout;
49import android.text.TextPaint;
50import android.text.TextUtils;
51import android.text.TextUtils.TruncateAt;
52import android.text.format.DateUtils;
53import android.text.style.BackgroundColorSpan;
54import android.text.style.CharacterStyle;
55import android.text.style.ForegroundColorSpan;
56import android.text.style.TextAppearanceSpan;
57import android.util.SparseArray;
58import android.util.TypedValue;
59import android.view.DragEvent;
60import android.view.MotionEvent;
61import android.view.View;
62import android.view.ViewGroup;
63import android.view.ViewParent;
64import android.view.animation.DecelerateInterpolator;
65import android.widget.AbsListView;
66import android.widget.AbsListView.OnScrollListener;
67import android.widget.TextView;
68
69import com.android.mail.R;
70import com.android.mail.analytics.Analytics;
71import com.android.mail.bitmap.AttachmentDrawable;
72import com.android.mail.bitmap.AttachmentGridDrawable;
73import com.android.mail.bitmap.ContactCheckableGridDrawable;
74import com.android.mail.bitmap.ContactDrawable;
75import com.android.mail.perf.Timer;
76import com.android.mail.providers.Attachment;
77import com.android.mail.providers.Conversation;
78import com.android.mail.providers.Folder;
79import com.android.mail.providers.UIProvider;
80import com.android.mail.providers.UIProvider.AttachmentRendition;
81import com.android.mail.providers.UIProvider.ConversationColumns;
82import com.android.mail.providers.UIProvider.ConversationListIcon;
83import com.android.mail.providers.UIProvider.FolderType;
84import com.android.mail.ui.AnimatedAdapter;
85import com.android.mail.ui.ControllableActivity;
86import com.android.mail.ui.ConversationSelectionSet;
87import com.android.mail.ui.ConversationSetObserver;
88import com.android.mail.ui.DividedImageCanvas;
89import com.android.mail.ui.DividedImageCanvas.InvalidateCallback;
90import com.android.mail.ui.FolderDisplayer;
91import com.android.mail.ui.SwipeableItemView;
92import com.android.mail.ui.SwipeableListView;
93import com.android.mail.ui.ViewMode;
94import com.android.mail.utils.FolderUri;
95import com.android.mail.utils.HardwareLayerEnabler;
96import com.android.mail.utils.LogTag;
97import com.android.mail.utils.LogUtils;
98import com.android.mail.utils.Utils;
99import com.android.mail.utils.ViewUtils;
100import com.google.common.annotations.VisibleForTesting;
101
102import java.util.ArrayList;
103import java.util.Iterator;
104import java.util.List;
105
106public class ConversationItemView extends View
107        implements SwipeableItemView, ToggleableItem, InvalidateCallback, OnScrollListener,
108        ConversationSetObserver, BadgeSpan.BadgeSpanDimensions {
109
110    // Timer.
111    private static int sLayoutCount = 0;
112    private static Timer sTimer; // Create the sTimer here if you need to do
113                                 // perf analysis.
114    private static final int PERF_LAYOUT_ITERATIONS = 50;
115    private static final String PERF_TAG_LAYOUT = "CCHV.layout";
116    private static final String PERF_TAG_CALCULATE_TEXTS_BITMAPS = "CCHV.txtsbmps";
117    private static final String PERF_TAG_CALCULATE_SENDER_SUBJECT = "CCHV.sendersubj";
118    private static final String PERF_TAG_CALCULATE_FOLDERS = "CCHV.folders";
119    private static final String PERF_TAG_CALCULATE_COORDINATES = "CCHV.coordinates";
120    private static final String LOG_TAG = LogTag.getLogTag();
121
122    // Static bitmaps.
123    private static Bitmap STAR_OFF;
124    private static Bitmap STAR_ON;
125    private static Bitmap ATTACHMENT;
126    private static Bitmap ONLY_TO_ME;
127    private static Bitmap TO_ME_AND_OTHERS;
128    private static Bitmap IMPORTANT_ONLY_TO_ME;
129    private static Bitmap IMPORTANT_TO_ME_AND_OTHERS;
130    private static Bitmap IMPORTANT;
131    private static Bitmap STATE_REPLIED;
132    private static Bitmap STATE_FORWARDED;
133    private static Bitmap STATE_REPLIED_AND_FORWARDED;
134    private static Bitmap STATE_CALENDAR_INVITE;
135    private static Bitmap VISIBLE_CONVERSATION_CARET;
136    private static Drawable RIGHT_EDGE_TABLET;
137    private static Drawable PLACEHOLDER;
138    private static Drawable PROGRESS_BAR;
139
140    private static String sSendersSplitToken;
141    private static String sElidedPaddingToken;
142
143    // Static colors.
144    private static int sSendersTextColorRead;
145    private static int sSendersTextColorUnread;
146    private static int sDateTextColor;
147    private static int sStarTouchSlop;
148    private static int sSenderImageTouchSlop;
149    private static int sShrinkAnimationDuration;
150    private static int sSlideAnimationDuration;
151    private static int sCabAnimationDuration;
152    private static int sBadgePaddingExtraWidth;
153    private static int sBadgeRoundedCornerRadius;
154
155    // Static paints.
156    private static final TextPaint sPaint = new TextPaint();
157    private static final TextPaint sFoldersPaint = new TextPaint();
158    private static final Paint sCheckBackgroundPaint = new Paint();
159
160    private static BroadcastReceiver sConfigurationChangedReceiver;
161
162    // Backgrounds for different states.
163    private final SparseArray<Drawable> mBackgrounds = new SparseArray<Drawable>();
164
165    // Dimensions and coordinates.
166    private int mViewWidth = -1;
167    /** The view mode at which we calculated mViewWidth previously. */
168    private int mPreviousMode;
169
170    private int mInfoIconX;
171    private int mDateX;
172    private int mDateWidth;
173    private int mPaperclipX;
174    private int mSendersX;
175    private int mSendersWidth;
176
177    /** Whether we are on a tablet device or not */
178    private final boolean mTabletDevice;
179    /** Whether we are on an expansive tablet */
180    private final boolean mIsExpansiveTablet;
181    /** When in conversation mode, true if the list is hidden */
182    private final boolean mListCollapsible;
183
184    @VisibleForTesting
185    ConversationItemViewCoordinates mCoordinates;
186
187    private ConversationItemViewCoordinates.Config mConfig;
188
189    private final Context mContext;
190
191    public ConversationItemViewModel mHeader;
192    private boolean mDownEvent;
193    private boolean mSelected = false;
194    private ConversationSelectionSet mSelectedConversationSet;
195    private Folder mDisplayedFolder;
196    private boolean mStarEnabled;
197    private boolean mSwipeEnabled;
198    private int mLastTouchX;
199    private int mLastTouchY;
200    private AnimatedAdapter mAdapter;
201    private float mAnimatedHeightFraction = 1.0f;
202    private final String mAccount;
203    private ControllableActivity mActivity;
204    private final TextView mSubjectTextView;
205    private final TextView mSendersTextView;
206    private final TextView mBadgeTextView;
207    private int mGadgetMode;
208    private boolean mAttachmentPreviewsEnabled;
209    private boolean mParallaxSpeedAlternative;
210    private boolean mParallaxDirectionAlternative;
211
212    private static int sFoldersStartPadding;
213    private static TextAppearanceSpan sSubjectTextUnreadSpan;
214    private static TextAppearanceSpan sSubjectTextReadSpan;
215    private static TextAppearanceSpan sBadgeTextSpan;
216    private static BackgroundColorSpan sBadgeBackgroundSpan;
217    private static ForegroundColorSpan sSnippetTextUnreadSpan;
218    private static ForegroundColorSpan sSnippetTextReadSpan;
219    private static int sScrollSlop;
220    private static CharacterStyle sActivatedTextSpan;
221
222    private final ContactCheckableGridDrawable mSendersImageView;
223    private final AttachmentGridDrawable mAttachmentsView;
224
225    /** The resource id of the color to use to override the background. */
226    private int mBackgroundOverrideResId = -1;
227    /** The bitmap to use, or <code>null</code> for the default */
228    private Bitmap mPhotoBitmap = null;
229    private Rect mPhotoRect = null;
230
231    /**
232     * A listener for clicks on the various areas of a conversation item.
233     */
234    public interface ConversationItemAreaClickListener {
235        /** Called when the info icon is clicked. */
236        void onInfoIconClicked();
237
238        /** Called when the star is clicked. */
239        void onStarClicked();
240    }
241
242    /** If set, it will steal all clicks for which the interface has a click method. */
243    private ConversationItemAreaClickListener mConversationItemAreaClickListener = null;
244
245    static {
246        sPaint.setAntiAlias(true);
247        sFoldersPaint.setAntiAlias(true);
248
249        sCheckBackgroundPaint.setColor(Color.GRAY);
250    }
251
252    /**
253     * Handles displaying folders in a conversation header view.
254     */
255    static class ConversationItemFolderDisplayer extends FolderDisplayer {
256
257        private int mFoldersCount;
258
259        public ConversationItemFolderDisplayer(Context context) {
260            super(context);
261        }
262
263        @Override
264        public void loadConversationFolders(Conversation conv, final FolderUri ignoreFolderUri,
265                final int ignoreFolderType) {
266            super.loadConversationFolders(conv, ignoreFolderUri, ignoreFolderType);
267            mFoldersCount = mFoldersSortedSet.size();
268        }
269
270        @Override
271        public void reset() {
272            super.reset();
273            mFoldersCount = 0;
274        }
275
276        public boolean hasVisibleFolders() {
277            return mFoldersCount > 0;
278        }
279
280        private int measureFolders(int availableSpace, int cellSize) {
281            int totalWidth = 0;
282            boolean firstTime = true;
283            for (Folder f : mFoldersSortedSet) {
284                final String folderString = f.name;
285                int width = (int) sFoldersPaint.measureText(folderString) + cellSize;
286                if (firstTime) {
287                    firstTime = false;
288                } else {
289                    width += sFoldersStartPadding;
290                }
291                totalWidth += width;
292                if (totalWidth > availableSpace) {
293                    break;
294                }
295            }
296
297            return totalWidth;
298        }
299
300        public void drawFolders(
301                Canvas canvas, ConversationItemViewCoordinates coordinates, boolean isRtl) {
302            if (mFoldersCount == 0) {
303                return;
304            }
305            final int left = coordinates.foldersLeft;
306            final int right = coordinates.foldersRight;
307            final int y = coordinates.foldersY;
308            final int height = coordinates.foldersHeight;
309            final int textBottomPadding = coordinates.foldersTextBottomPadding;
310
311            sFoldersPaint.setTextSize(coordinates.foldersFontSize);
312            sFoldersPaint.setTypeface(coordinates.foldersTypeface);
313
314            // Initialize space and cell size based on the current mode.
315            int availableSpace = right - left;
316            int maxFoldersCount = availableSpace / coordinates.getFolderMinimumWidth();
317            int foldersCount = Math.min(mFoldersCount, maxFoldersCount);
318            int averageWidth = availableSpace / foldersCount;
319            int cellSize = coordinates.getFolderCellWidth();
320
321            // TODO(ath): sFoldersPaint.measureText() is done 3x in this method. stop that.
322            // Extra credit: maybe cache results across items as long as font size doesn't change.
323
324            final int totalWidth = measureFolders(availableSpace, cellSize);
325            int xLeft = (isRtl) ? 0 : right - Math.min(availableSpace, totalWidth);
326            final boolean overflow = totalWidth > availableSpace;
327
328            // Second pass to draw folders.
329            int i = 0;
330            for (Iterator<Folder> it = isRtl ?
331                    mFoldersSortedSet.descendingIterator() : mFoldersSortedSet.iterator();
332                    it.hasNext();) {
333                final Folder f = it.next();
334                if (availableSpace <= 0) {
335                    break;
336                }
337                final String folderString = f.name;
338                final int fgColor = f.getForegroundColor(mDefaultFgColor);
339                final int bgColor = f.getBackgroundColor(mDefaultBgColor);
340                boolean labelTooLong = false;
341                final int textW = (int) sFoldersPaint.measureText(folderString);
342                int width = textW + cellSize + sFoldersStartPadding;
343
344                if (overflow && width > averageWidth) {
345                    if (i < foldersCount - 1) {
346                        width = averageWidth;
347                    } else {
348                        // allow the last label to take all remaining space
349                        // (and don't let it make room for padding)
350                        width = availableSpace + sFoldersStartPadding;
351                    }
352                    labelTooLong = true;
353                }
354
355                // Draw the box.
356                sFoldersPaint.setColor(bgColor);
357                sFoldersPaint.setStyle(Paint.Style.FILL);
358                canvas.drawRect(xLeft, y, xLeft + width - sFoldersStartPadding,
359                        y + height, sFoldersPaint);
360
361                // Draw the text.
362                final int padding = cellSize / 2;
363                sFoldersPaint.setColor(fgColor);
364                sFoldersPaint.setStyle(Paint.Style.FILL);
365                if (labelTooLong) {
366                    // todo - take RTL into account for fade
367                    final int rightBorder = xLeft + width - sFoldersStartPadding - padding;
368                    final Shader shader = new LinearGradient(rightBorder - padding, y,
369                            rightBorder, y, fgColor, Utils.getTransparentColor(fgColor),
370                            Shader.TileMode.CLAMP);
371                    sFoldersPaint.setShader(shader);
372                }
373                canvas.drawText(folderString, xLeft + padding, y + height - textBottomPadding,
374                        sFoldersPaint);
375                if (labelTooLong) {
376                    sFoldersPaint.setShader(null);
377                }
378
379                availableSpace -= width;
380                xLeft += width;
381                i++;
382            }
383        }
384    }
385
386    public ConversationItemView(Context context, String account) {
387        super(context);
388        Utils.traceBeginSection("CIVC constructor");
389        setClickable(true);
390        setLongClickable(true);
391        mContext = context.getApplicationContext();
392        final Resources res = mContext.getResources();
393        mTabletDevice = Utils.useTabletUI(res);
394        mIsExpansiveTablet =
395                mTabletDevice ? res.getBoolean(R.bool.use_expansive_tablet_ui) : false;
396        mListCollapsible = res.getBoolean(R.bool.list_collapsible);
397        mAccount = account;
398
399        getItemViewResources(mContext);
400
401        mSendersTextView = new TextView(mContext);
402        mSendersTextView.setIncludeFontPadding(false);
403
404        mSubjectTextView = new TextView(mContext);
405        mSubjectTextView.setEllipsize(TextUtils.TruncateAt.END);
406        mSubjectTextView.setIncludeFontPadding(false);
407
408        mBadgeTextView = new TextView(mContext);
409        mBadgeTextView.setIncludeFontPadding(false);
410
411        mAttachmentsView = new AttachmentGridDrawable(res, PLACEHOLDER, PROGRESS_BAR);
412        mAttachmentsView.setCallback(this);
413
414        mSendersImageView = new ContactCheckableGridDrawable(res, sCabAnimationDuration);
415        mSendersImageView.setCallback(this);
416
417        Utils.traceEndSection();
418    }
419
420    private static synchronized void getItemViewResources(Context context) {
421        if (sConfigurationChangedReceiver == null) {
422            sConfigurationChangedReceiver = new BroadcastReceiver() {
423                @Override
424                public void onReceive(Context context, Intent intent) {
425                    STAR_OFF = null;
426                    getItemViewResources(context);
427                }
428            };
429            context.registerReceiver(sConfigurationChangedReceiver, new IntentFilter(
430                    Intent.ACTION_CONFIGURATION_CHANGED));
431        }
432        if (STAR_OFF == null) {
433            final Resources res = context.getResources();
434            // Initialize static bitmaps.
435            STAR_OFF = BitmapFactory.decodeResource(res, R.drawable.ic_btn_star_off);
436            STAR_ON = BitmapFactory.decodeResource(res, R.drawable.ic_btn_star_on);
437            ATTACHMENT = BitmapFactory.decodeResource(res, R.drawable.ic_attachment_holo_light);
438            ONLY_TO_ME = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_double);
439            TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_single);
440            IMPORTANT_ONLY_TO_ME = BitmapFactory.decodeResource(res,
441                    R.drawable.ic_email_caret_double_important_unread);
442            IMPORTANT_TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res,
443                    R.drawable.ic_email_caret_single_important_unread);
444            IMPORTANT = BitmapFactory.decodeResource(res,
445                    R.drawable.ic_email_caret_none_important_unread);
446            STATE_REPLIED =
447                    BitmapFactory.decodeResource(res, R.drawable.ic_badge_reply_holo_light);
448            STATE_FORWARDED =
449                    BitmapFactory.decodeResource(res, R.drawable.ic_badge_forward_holo_light);
450            STATE_REPLIED_AND_FORWARDED =
451                    BitmapFactory.decodeResource(res, R.drawable.ic_badge_reply_forward_holo_light);
452            STATE_CALENDAR_INVITE =
453                    BitmapFactory.decodeResource(res, R.drawable.ic_badge_invite_holo_light);
454            VISIBLE_CONVERSATION_CARET = BitmapFactory.decodeResource(res, R.drawable.caret_grey);
455            RIGHT_EDGE_TABLET = res.getDrawable(R.drawable.list_edge_tablet);
456            PLACEHOLDER = res.getDrawable(R.drawable.ic_attachment_load);
457            PROGRESS_BAR = res.getDrawable(R.drawable.progress_holo);
458
459            // Initialize colors.
460            sActivatedTextSpan = CharacterStyle.wrap(new ForegroundColorSpan(
461                    res.getColor(R.color.senders_text_color_read)));
462            sSendersTextColorRead = res.getColor(R.color.senders_text_color_read);
463            sSendersTextColorUnread = res.getColor(R.color.senders_text_color_unread);
464            sSubjectTextUnreadSpan = new TextAppearanceSpan(context,
465                    R.style.SubjectAppearanceUnreadStyle);
466            sBadgeTextSpan = new TextAppearanceSpan(context, R.style.BadgeTextStyle);
467            sBadgeBackgroundSpan = new BackgroundColorSpan(
468                    res.getColor(R.color.badge_background_color));
469            sSubjectTextReadSpan = new TextAppearanceSpan(
470                    context, R.style.SubjectAppearanceReadStyle);
471            sSnippetTextUnreadSpan =
472                    new ForegroundColorSpan(res.getColor(R.color.snippet_text_color_unread));
473            sSnippetTextReadSpan =
474                    new ForegroundColorSpan(res.getColor(R.color.snippet_text_color_read));
475            sDateTextColor = res.getColor(R.color.date_text_color);
476            sStarTouchSlop = res.getDimensionPixelSize(R.dimen.star_touch_slop);
477            sSenderImageTouchSlop = res.getDimensionPixelSize(R.dimen.sender_image_touch_slop);
478            sShrinkAnimationDuration = res.getInteger(R.integer.shrink_animation_duration);
479            sSlideAnimationDuration = res.getInteger(R.integer.slide_animation_duration);
480            // Initialize static color.
481            sSendersSplitToken = res.getString(R.string.senders_split_token);
482            sElidedPaddingToken = res.getString(R.string.elided_padding_token);
483            sScrollSlop = res.getInteger(R.integer.swipeScrollSlop);
484            sFoldersStartPadding = res.getDimensionPixelOffset(R.dimen.folders_start_padding);
485            sCabAnimationDuration = res.getInteger(R.integer.conv_item_view_cab_anim_duration);
486            sBadgePaddingExtraWidth = res.getDimensionPixelSize(R.dimen.badge_padding_extra_width);
487            sBadgeRoundedCornerRadius =
488                    res.getDimensionPixelSize(R.dimen.badge_rounded_corner_radius);
489        }
490    }
491
492    public void bind(final Conversation conversation, final ControllableActivity activity,
493            final ConversationSelectionSet set, final Folder folder,
494            final int checkboxOrSenderImage, final boolean showAttachmentPreviews,
495            final boolean parallaxSpeedAlternative, final boolean parallaxDirectionAlternative,
496            final boolean swipeEnabled, final boolean importanceMarkersEnabled,
497            final boolean showChevronsEnabled, final AnimatedAdapter adapter) {
498        Utils.traceBeginSection("CIVC.bind");
499        bind(ConversationItemViewModel.forConversation(mAccount, conversation), activity,
500                null /* conversationItemAreaClickListener */,
501                set, folder, checkboxOrSenderImage, showAttachmentPreviews,
502                parallaxSpeedAlternative, parallaxDirectionAlternative, swipeEnabled,
503                importanceMarkersEnabled, showChevronsEnabled,
504                adapter, -1 /* backgroundOverrideResId */, null /* photoBitmap */,
505                false /* useFullMargins */);
506        Utils.traceEndSection();
507    }
508
509    public void bindAd(final ConversationItemViewModel conversationItemViewModel,
510            final ControllableActivity activity,
511            final ConversationItemAreaClickListener conversationItemAreaClickListener,
512            final Folder folder, final int checkboxOrSenderImage, final AnimatedAdapter adapter,
513            final int backgroundOverrideResId, final Bitmap photoBitmap) {
514        Utils.traceBeginSection("CIVC.bindAd");
515        bind(conversationItemViewModel, activity, conversationItemAreaClickListener, null /* set */,
516                folder, checkboxOrSenderImage, false /* attachment previews */,
517                false /* parallax */, false /* parallax */, true /* swipeEnabled */,
518                false /* importanceMarkersEnabled */, false /* showChevronsEnabled */,
519                adapter, backgroundOverrideResId, photoBitmap, true /* useFullMargins */);
520        Utils.traceEndSection();
521    }
522
523    private void bind(final ConversationItemViewModel header, final ControllableActivity activity,
524            final ConversationItemAreaClickListener conversationItemAreaClickListener,
525            final ConversationSelectionSet set, final Folder folder,
526            final int checkboxOrSenderImage, final boolean showAttachmentPreviews,
527            final boolean parallaxSpeedAlternative, final boolean parallaxDirectionAlternative,
528            boolean swipeEnabled, final boolean importanceMarkersEnabled,
529            final boolean showChevronsEnabled, final AnimatedAdapter adapter,
530            final int backgroundOverrideResId, final Bitmap photoBitmap,
531            final boolean useFullMargins) {
532        mBackgroundOverrideResId = backgroundOverrideResId;
533        mPhotoBitmap = photoBitmap;
534        mConversationItemAreaClickListener = conversationItemAreaClickListener;
535
536        if (mHeader != null) {
537            Utils.traceBeginSection("unbind");
538            final boolean newlyBound = header.conversation.id != mHeader.conversation.id;
539            // If this was previously bound to a different conversation, remove any contact photo
540            // manager requests.
541            if (newlyBound || (mHeader.displayableNames != null && !mHeader
542                    .displayableNames.equals(header.displayableNames))) {
543                for (int i = 0; i < mSendersImageView.getCount(); i++) {
544                    mSendersImageView.getOrCreateDrawable(i).unbind();
545                }
546                mSendersImageView.setCount(0);
547            }
548
549            // If this was previously bound to a different conversation,
550            // remove any attachment preview manager requests.
551            if (newlyBound || header.conversation.attachmentPreviewsCount
552                    != mHeader.conversation.attachmentPreviewsCount || !header.conversation
553                    .getAttachmentPreviewUris().equals(
554                            mHeader.conversation.getAttachmentPreviewUris())) {
555
556                // unbind the attachments view (releasing bitmap references)
557                // (this also cancels all async tasks)
558                for (int i = 0, len = mAttachmentsView.getCount(); i < len; i++) {
559                    mAttachmentsView.getOrCreateDrawable(i).unbind();
560                }
561                // reset the grid, as the newly bound item may have a different attachment count
562                mAttachmentsView.setCount(0);
563            }
564
565            if (newlyBound) {
566                // Stop the photo flip animation
567                final boolean showSenders = !isSelected();
568                mSendersImageView.reset(showSenders);
569            }
570            Utils.traceEndSection();
571        }
572        mCoordinates = null;
573        mHeader = header;
574        mActivity = activity;
575        mSelectedConversationSet = set;
576        if (mSelectedConversationSet != null) {
577            mSelectedConversationSet.addObserver(this);
578        }
579        mDisplayedFolder = folder;
580        mStarEnabled = folder != null && !folder.isTrash();
581        mSwipeEnabled = swipeEnabled;
582        mAdapter = adapter;
583
584        Utils.traceBeginSection("drawables");
585        mAttachmentsView.setBitmapCache(mAdapter.getAttachmentPreviewsCache());
586        mAttachmentsView.setDecodeAggregator(mAdapter.getAttachmentPreviewsDecodeAggregator());
587        mSendersImageView.setBitmapCache(mAdapter.getSendersImagesCache());
588        mSendersImageView.setContactResolver(mAdapter.getContactResolver());
589        Utils.traceEndSection();
590
591        if (checkboxOrSenderImage == ConversationListIcon.SENDER_IMAGE) {
592            mGadgetMode = ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO;
593        } else {
594            mGadgetMode = ConversationItemViewCoordinates.GADGET_NONE;
595        }
596
597        mAttachmentPreviewsEnabled = showAttachmentPreviews;
598        mParallaxSpeedAlternative = parallaxSpeedAlternative;
599        mParallaxDirectionAlternative = parallaxDirectionAlternative;
600
601        Utils.traceBeginSection("folder displayer");
602        // Initialize folder displayer.
603        if (mHeader.folderDisplayer == null) {
604            mHeader.folderDisplayer = new ConversationItemFolderDisplayer(mContext);
605        } else {
606            mHeader.folderDisplayer.reset();
607        }
608        Utils.traceEndSection();
609
610        final int ignoreFolderType;
611        if (mDisplayedFolder.isInbox()) {
612            ignoreFolderType = FolderType.INBOX;
613        } else {
614            ignoreFolderType = -1;
615        }
616
617        Utils.traceBeginSection("load folders");
618        mHeader.folderDisplayer.loadConversationFolders(mHeader.conversation,
619                mDisplayedFolder.folderUri, ignoreFolderType);
620        Utils.traceEndSection();
621
622        if (mHeader.showDateText) {
623            Utils.traceBeginSection("relative time");
624            mHeader.dateText = DateUtils.getRelativeTimeSpanString(mContext,
625                    mHeader.conversation.dateMs);
626            Utils.traceEndSection();
627        } else {
628            mHeader.dateText = "";
629        }
630
631        Utils.traceBeginSection("config setup");
632        mConfig = new ConversationItemViewCoordinates.Config()
633            .withGadget(mGadgetMode)
634            .withAttachmentPreviews(getAttachmentPreviewsMode())
635            .setUseFullMargins(useFullMargins);
636        if (header.folderDisplayer.hasVisibleFolders()) {
637            mConfig.showFolders();
638        }
639        if (header.hasBeenForwarded || header.hasBeenRepliedTo || header.isInvite) {
640            mConfig.showReplyState();
641        }
642        if (mHeader.conversation.color != 0) {
643            mConfig.showColorBlock();
644        }
645
646        // Importance markers and chevrons (personal level indicators).
647        mHeader.personalLevelBitmap = null;
648        final int personalLevel = mHeader.conversation.personalLevel;
649        final boolean isImportant =
650                mHeader.conversation.priority == UIProvider.ConversationPriority.IMPORTANT;
651        final boolean useImportantMarkers = isImportant && importanceMarkersEnabled;
652        if (showChevronsEnabled &&
653                personalLevel == UIProvider.ConversationPersonalLevel.ONLY_TO_ME) {
654            mHeader.personalLevelBitmap = useImportantMarkers ? IMPORTANT_ONLY_TO_ME
655                    : ONLY_TO_ME;
656        } else if (showChevronsEnabled &&
657                personalLevel == UIProvider.ConversationPersonalLevel.TO_ME_AND_OTHERS) {
658            mHeader.personalLevelBitmap = useImportantMarkers ? IMPORTANT_TO_ME_AND_OTHERS
659                    : TO_ME_AND_OTHERS;
660        } else if (useImportantMarkers) {
661            mHeader.personalLevelBitmap = IMPORTANT;
662        }
663        if (mHeader.personalLevelBitmap != null) {
664            mConfig.showPersonalIndicator();
665        }
666        Utils.traceEndSection();
667
668        Utils.traceBeginSection("overflow");
669        mAttachmentsView.setOverflowText(null);
670        Utils.traceEndSection();
671
672        Utils.traceBeginSection("content description");
673        setContentDescription();
674        Utils.traceEndSection();
675        requestLayout();
676    }
677
678    @Override
679    protected void onDetachedFromWindow() {
680        super.onDetachedFromWindow();
681
682        if (mSelectedConversationSet != null) {
683            mSelectedConversationSet.removeObserver(this);
684        }
685    }
686
687    @Override
688    public void invalidateDrawable(final Drawable who) {
689        boolean handled = false;
690        if (mCoordinates != null) {
691            if (mAttachmentsView.equals(who)) {
692                final Rect r = new Rect(who.getBounds());
693                r.offset(mCoordinates.attachmentPreviewsX, mCoordinates.attachmentPreviewsY);
694                ConversationItemView.this.invalidate(r.left, r.top, r.right, r.bottom);
695                handled = true;
696            } else if (mSendersImageView.equals(who)) {
697                final Rect r = new Rect(who.getBounds());
698                r.offset(mCoordinates.contactImagesX, mCoordinates.contactImagesY);
699                ConversationItemView.this.invalidate(r.left, r.top, r.right, r.bottom);
700                handled = true;
701            }
702        }
703        if (!handled) {
704            super.invalidateDrawable(who);
705        }
706    }
707
708    /**
709     * Get the Conversation object associated with this view.
710     */
711    public Conversation getConversation() {
712        return mHeader.conversation;
713    }
714
715    private static void startTimer(String tag) {
716        if (sTimer != null) {
717            sTimer.start(tag);
718        }
719    }
720
721    private static void pauseTimer(String tag) {
722        if (sTimer != null) {
723            sTimer.pause(tag);
724        }
725    }
726
727    @Override
728    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
729        Utils.traceBeginSection("CIVC.measure");
730        final int wSize = MeasureSpec.getSize(widthMeasureSpec);
731
732        final int currentMode = mActivity.getViewMode().getMode();
733        if (wSize != mViewWidth || mPreviousMode != currentMode) {
734            mViewWidth = wSize;
735            mPreviousMode = currentMode;
736        }
737        mHeader.viewWidth = mViewWidth;
738
739        mConfig.updateWidth(wSize).setViewMode(currentMode)
740                .setLayoutDirection(ViewCompat.getLayoutDirection(this));
741
742        Resources res = getResources();
743        mHeader.standardScaledDimen = res.getDimensionPixelOffset(R.dimen.standard_scaled_dimen);
744
745        mCoordinates = ConversationItemViewCoordinates.forConfig(mContext, mConfig,
746                mAdapter.getCoordinatesCache());
747
748        if (mPhotoBitmap != null) {
749            mPhotoRect = new Rect(0, 0, mCoordinates.contactImagesWidth,
750                    mCoordinates.contactImagesHeight);
751        }
752
753        final int h = (mAnimatedHeightFraction != 1.0f) ?
754                Math.round(mAnimatedHeightFraction * mCoordinates.height) : mCoordinates.height;
755        setMeasuredDimension(mConfig.getWidth(), h);
756        Utils.traceEndSection();
757    }
758
759    @Override
760    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
761        startTimer(PERF_TAG_LAYOUT);
762        Utils.traceBeginSection("CIVC.layout");
763
764        super.onLayout(changed, left, top, right, bottom);
765
766        Utils.traceBeginSection("text and bitmaps");
767        calculateTextsAndBitmaps();
768        Utils.traceEndSection();
769
770        Utils.traceBeginSection("coordinates");
771        calculateCoordinates();
772        Utils.traceEndSection();
773
774        // Subject.
775        Utils.traceBeginSection("subject");
776        createSubject(mHeader.unread);
777
778        createBadge();
779
780        if (!mHeader.isLayoutValid()) {
781            setContentDescription();
782        }
783        mHeader.validate();
784        Utils.traceEndSection();
785
786        pauseTimer(PERF_TAG_LAYOUT);
787        if (sTimer != null && ++sLayoutCount >= PERF_LAYOUT_ITERATIONS) {
788            sTimer.dumpResults();
789            sTimer = new Timer();
790            sLayoutCount = 0;
791        }
792        Utils.traceEndSection();
793    }
794
795    private void setContentDescription() {
796        if (mActivity.isAccessibilityEnabled()) {
797            mHeader.resetContentDescription();
798            setContentDescription(
799                    mHeader.getContentDescription(mContext, mDisplayedFolder.shouldShowRecipients()));
800        }
801    }
802
803    @Override
804    public void setBackgroundResource(int resourceId) {
805        Utils.traceBeginSection("set background resource");
806        Drawable drawable = mBackgrounds.get(resourceId);
807        if (drawable == null) {
808            drawable = getResources().getDrawable(resourceId);
809            final int insetPadding = mHeader.insetPadding;
810            if (insetPadding > 0) {
811                drawable = new InsetDrawable(drawable, insetPadding);
812            }
813            mBackgrounds.put(resourceId, drawable);
814        }
815        if (getBackground() != drawable) {
816            super.setBackgroundDrawable(drawable);
817        }
818        Utils.traceEndSection();
819    }
820
821    private void calculateTextsAndBitmaps() {
822        startTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
823
824        if (mSelectedConversationSet != null) {
825            mSelected = mSelectedConversationSet.contains(mHeader.conversation);
826        }
827        setSelected(mSelected);
828        mHeader.gadgetMode = mGadgetMode;
829
830        updateBackground();
831
832        mHeader.sendersDisplayText = new SpannableStringBuilder();
833
834        mHeader.hasDraftMessage = mHeader.conversation.numDrafts() > 0;
835
836        // Parse senders fragments.
837        if (mHeader.preserveSendersText) {
838            // This is a special view that doesn't need special sender formatting
839            mHeader.sendersDisplayText = new SpannableStringBuilder(mHeader.sendersText);
840            loadImages();
841        } else if (mHeader.conversation.conversationInfo != null) {
842            Context context = getContext();
843            mHeader.messageInfoString = SendersView
844                    .createMessageInfo(context, mHeader.conversation, true);
845            int maxChars = ConversationItemViewCoordinates.getSendersLength(context,
846                    mCoordinates.getMode(), mHeader.conversation.hasAttachments);
847            mHeader.displayableEmails = new ArrayList<String>();
848            mHeader.displayableNames = new ArrayList<String>();
849            mHeader.styledNames = new ArrayList<SpannableString>();
850
851            SendersView.format(context, mHeader.conversation.conversationInfo,
852                    mHeader.messageInfoString.toString(), maxChars, mHeader.styledNames,
853                    mHeader.displayableNames, mHeader.displayableEmails, mAccount,
854                    mDisplayedFolder.shouldShowRecipients(), true);
855
856            if (mHeader.displayableEmails.isEmpty() && mHeader.hasDraftMessage) {
857                mHeader.displayableEmails.add(mAccount);
858                mHeader.displayableNames.add(mAccount);
859            }
860
861            // If we have displayable senders, load their thumbnails
862            loadImages();
863        } else {
864            LogUtils.wtf(LOG_TAG, "Null conversationInfo");
865        }
866
867        if (isAttachmentPreviewsEnabled()) {
868            loadAttachmentPreviews();
869        }
870
871        if (mHeader.isLayoutValid()) {
872            pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
873            return;
874        }
875        startTimer(PERF_TAG_CALCULATE_FOLDERS);
876
877
878        pauseTimer(PERF_TAG_CALCULATE_FOLDERS);
879
880        // Paper clip icon.
881        mHeader.paperclip = null;
882        if (mHeader.conversation.hasAttachments) {
883            mHeader.paperclip = ATTACHMENT;
884        }
885
886        startTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT);
887
888        pauseTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT);
889        pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
890    }
891
892    private boolean isAttachmentPreviewsEnabled() {
893        return mAttachmentPreviewsEnabled && !mHeader.conversation.getAttachmentPreviewUris()
894                .isEmpty();
895    }
896
897    private int getOverflowCount() {
898        return mHeader.conversation.attachmentPreviewsCount - mHeader.conversation
899                .getAttachmentPreviewUris().size();
900    }
901
902    private int getAttachmentPreviewsMode() {
903        if (isAttachmentPreviewsEnabled()) {
904            return mHeader.conversation.read
905                    ? ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_READ
906                    : ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_UNREAD;
907        } else {
908            return ConversationItemViewCoordinates.ATTACHMENT_PREVIEW_NONE;
909        }
910    }
911
912    private float getParallaxSpeedMultiplier() {
913        return mParallaxSpeedAlternative
914                ? SwipeableListView.ATTACHMENT_PARALLAX_MULTIPLIER_ALTERNATIVE
915                : SwipeableListView.ATTACHMENT_PARALLAX_MULTIPLIER_NORMAL;
916    }
917
918    // FIXME(ath): maybe move this to bind(). the only dependency on layout is on tile W/H, which
919    // is immutable.
920    private void loadImages() {
921        if (mGadgetMode != ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO
922                || mHeader.displayableEmails == null
923                || mHeader.displayableEmails.isEmpty()) {
924            return;
925        }
926        if (mCoordinates.contactImagesWidth <= 0 || mCoordinates.contactImagesHeight <= 0) {
927            LogUtils.w(LOG_TAG,
928                    "Contact image width(%d) or height(%d) is 0 for mode: (%d).",
929                    mCoordinates.contactImagesWidth, mCoordinates.contactImagesHeight,
930                    mCoordinates.getMode());
931            return;
932        }
933
934        Utils.traceBeginSection("load sender images");
935        final int count = mHeader.displayableEmails.size();
936
937        mSendersImageView.setCount(count);
938        mSendersImageView
939                .setBounds(0, 0, mCoordinates.contactImagesWidth, mCoordinates.contactImagesHeight);
940
941        for (int i = 0; i < DividedImageCanvas.MAX_DIVISIONS && i < count; i++) {
942            Utils.traceBeginSection("load single sender image");
943            final ContactDrawable drawable = mSendersImageView.getOrCreateDrawable(i);
944            drawable.setDecodeDimensions(mCoordinates.contactImagesWidth,
945                    mCoordinates.contactImagesHeight);
946            drawable.bind(mHeader.displayableNames.get(i),
947                    mHeader.displayableEmails.get(i));
948            Utils.traceEndSection();
949        }
950        Utils.traceEndSection();
951    }
952
953    private void loadAttachmentPreviews() {
954        if (mCoordinates.attachmentPreviewsWidth <= 0
955                || mCoordinates.attachmentPreviewsHeight <= 0) {
956            LogUtils.w(LOG_TAG,
957                    "Attachment preview width(%d) or height(%d) is 0 for mode: (%d,%d).",
958                    mCoordinates.attachmentPreviewsWidth, mCoordinates.attachmentPreviewsHeight,
959                    mCoordinates.getMode(), getAttachmentPreviewsMode());
960            return;
961        }
962        Utils.traceBeginSection("attachment previews");
963
964        Utils.traceBeginSection("Setup load attachment previews");
965
966        LogUtils.d(LOG_TAG,
967                "loadAttachmentPreviews: Loading attachment previews for conversation %s",
968                mHeader.conversation);
969
970        // Get list of attachments and states from conversation
971        final ArrayList<String> attachmentUris = mHeader.conversation.getAttachmentPreviewUris();
972        final int previewStates = mHeader.conversation.attachmentPreviewStates;
973        final int displayCount = Math.min(
974                attachmentUris.size(), AttachmentGridDrawable.MAX_VISIBLE_ATTACHMENT_COUNT);
975        Utils.traceEndSection();
976
977        mAttachmentsView.setCoordinates(mCoordinates);
978        mAttachmentsView.setCount(displayCount);
979
980        final int decodeHeight;
981        // if parallax is enabled, increase the desired vertical size of attachment bitmaps
982        // so we have extra pixels to scroll within
983        if (SwipeableListView.ENABLE_ATTACHMENT_PARALLAX) {
984            decodeHeight = Math.round(mCoordinates.attachmentPreviewsDecodeHeight
985                    * getParallaxSpeedMultiplier());
986        } else {
987            decodeHeight = mCoordinates.attachmentPreviewsDecodeHeight;
988        }
989
990        // set the bounds before binding inner drawables so they can decode right away
991        // (they need the their bounds set to know whether to decode to 1x1 or 2x1 dimens)
992        mAttachmentsView.setBounds(0, 0, mCoordinates.attachmentPreviewsWidth,
993                mCoordinates.attachmentPreviewsHeight);
994
995        for (int i = 0; i < displayCount; i++) {
996            Utils.traceBeginSection("setup single attachment preview");
997            final String uri = attachmentUris.get(i);
998
999            // Find the rendition to load based on availability.
1000            LogUtils.v(LOG_TAG, "loadAttachmentPreviews: state [BEST, SIMPLE] is [%s, %s] for %s ",
1001                    Attachment.getPreviewState(previewStates, i, AttachmentRendition.BEST),
1002                    Attachment.getPreviewState(previewStates, i, AttachmentRendition.SIMPLE),
1003                    uri);
1004            int bestAvailableRendition = -1;
1005            // BEST first, else use less preferred renditions
1006            for (final int rendition : AttachmentRendition.PREFERRED_RENDITIONS) {
1007                if (Attachment.getPreviewState(previewStates, i, rendition)) {
1008                    bestAvailableRendition = rendition;
1009                    break;
1010                }
1011            }
1012
1013            LogUtils.d(LOG_TAG,
1014                    "creating/setting drawable region in CIV=%s canvas=%s rend=%s uri=%s",
1015                    this, mAttachmentsView, bestAvailableRendition, uri);
1016            final AttachmentDrawable drawable = mAttachmentsView.getOrCreateDrawable(i);
1017            drawable.setDecodeDimensions(mCoordinates.attachmentPreviewsWidth, decodeHeight);
1018            drawable.setParallaxSpeedMultiplier(getParallaxSpeedMultiplier());
1019            if (bestAvailableRendition != -1) {
1020                drawable.bind(getContext(), uri, bestAvailableRendition);
1021            } else {
1022                drawable.showStaticPlaceholder();
1023            }
1024
1025            Utils.traceEndSection();
1026        }
1027
1028        Utils.traceEndSection();
1029    }
1030
1031    private static int makeExactSpecForSize(int size) {
1032        return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
1033    }
1034
1035    private static void layoutViewExactly(View v, int w, int h) {
1036        v.measure(makeExactSpecForSize(w), makeExactSpecForSize(h));
1037        v.layout(0, 0, w, h);
1038    }
1039
1040    private void layoutParticipantText(SpannableStringBuilder participantText) {
1041        if (participantText != null) {
1042            if (isActivated() && showActivatedText()) {
1043                participantText.setSpan(sActivatedTextSpan, 0,
1044                        mHeader.styledMessageInfoStringOffset, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1045            } else {
1046                participantText.removeSpan(sActivatedTextSpan);
1047            }
1048
1049            final int w = mSendersWidth;
1050            final int h = mCoordinates.sendersHeight;
1051            mSendersTextView.setLayoutParams(new ViewGroup.LayoutParams(w, h));
1052            mSendersTextView.setMaxLines(mCoordinates.sendersLineCount);
1053            mSendersTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCoordinates.sendersFontSize);
1054            layoutViewExactly(mSendersTextView, w, h);
1055
1056            mSendersTextView.setText(participantText);
1057        }
1058    }
1059
1060    private void createSubject(final boolean isUnread) {
1061        // Need to check if we're in wide mode because the badge
1062        // does not get added if we're in wide mode.
1063        final BidiFormatter bidiFormatter = mAdapter.getBidiFormatter();
1064        final String badgeText = mCoordinates.isWideMode() || mHeader.badgeText == null ? "" :
1065                bidiFormatter.unicodeWrap(mHeader.badgeText);
1066        final String subject = bidiFormatter.unicodeWrap(filterTag(mHeader.conversation.subject));
1067        final String snippet = bidiFormatter.unicodeWrap(mHeader.conversation.getSnippet());
1068        final Spannable displayedStringBuilder = new SpannableString(
1069                Conversation.getSubjectAndSnippetForDisplay(
1070                        mContext, badgeText, subject, snippet));
1071
1072        // since spans affect text metrics, add spans to the string before measure/layout or fancy
1073        // ellipsizing
1074
1075        final int badgeTextLength = formatBadgeText(displayedStringBuilder, badgeText);
1076
1077        final int subjectTextLength = badgeTextLength + ((subject != null) ? subject.length() : 0)
1078                + ((badgeTextLength > 0) ? 1 : 0);
1079        if (!TextUtils.isEmpty(subject)) {
1080            displayedStringBuilder.setSpan(TextAppearanceSpan.wrap(
1081                    isUnread ? sSubjectTextUnreadSpan : sSubjectTextReadSpan),
1082                    badgeTextLength, subjectTextLength, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1083        }
1084        if (!TextUtils.isEmpty(snippet)) {
1085            final int startOffset = subjectTextLength;
1086            // Start after the end of the subject text; since the subject may be
1087            // "" or null, this could start at the 0th character in the subjectText string
1088            displayedStringBuilder.setSpan(ForegroundColorSpan.wrap(
1089                    isUnread ? sSnippetTextUnreadSpan : sSnippetTextReadSpan), startOffset,
1090                    displayedStringBuilder.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1091        }
1092        if (isActivated() && showActivatedText()) {
1093            displayedStringBuilder.setSpan(sActivatedTextSpan, badgeTextLength,
1094                    displayedStringBuilder.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
1095        }
1096
1097        final int subjectWidth = mCoordinates.subjectWidth;
1098        final int subjectHeight = mCoordinates.subjectHeight;
1099        mSubjectTextView.setLayoutParams(new ViewGroup.LayoutParams(subjectWidth, subjectHeight));
1100        mSubjectTextView.setMaxLines(mCoordinates.subjectLineCount);
1101        mSubjectTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCoordinates.subjectFontSize);
1102        layoutViewExactly(mSubjectTextView, subjectWidth, subjectHeight);
1103
1104        mSubjectTextView.setText(displayedStringBuilder);
1105    }
1106
1107    private void createBadge() {
1108        // Do not create badge if in wide mode or badge text is empty.
1109        final String badgeText = mHeader.badgeText;
1110        if (!mCoordinates.isWideMode() || TextUtils.isEmpty(badgeText)) {
1111            return;
1112        }
1113
1114        final Spannable displayedBadgeString = new SpannableString(badgeText);
1115        formatBadgeText(displayedBadgeString, badgeText);
1116
1117        final int badgeWidth = mCoordinates.badgeWidth;
1118        final int badgeHeight = mCoordinates.badgeHeight;
1119        mBadgeTextView.setLayoutParams(new ViewGroup.LayoutParams(badgeWidth, badgeHeight));
1120        mBadgeTextView.setMaxLines(mCoordinates.badgeLineCount);
1121        layoutViewExactly(mBadgeTextView, badgeWidth, badgeHeight);
1122
1123        mBadgeTextView.setText(displayedBadgeString);
1124    }
1125
1126    private int formatBadgeText(Spannable displayedStringBuilder, String badgeText) {
1127        final int badgeTextLength = (badgeText != null) ? badgeText.length() : 0;
1128        if (!TextUtils.isEmpty(badgeText)) {
1129            displayedStringBuilder.setSpan(TextAppearanceSpan.wrap(sBadgeTextSpan),
1130                    0, badgeTextLength, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1131            displayedStringBuilder.setSpan(TextAppearanceSpan.wrap(sBadgeBackgroundSpan),
1132                    0, badgeTextLength, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1133            displayedStringBuilder.setSpan(new BadgeSpan(displayedStringBuilder, this),
1134                    0, badgeTextLength, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
1135        }
1136
1137        return badgeTextLength;
1138    }
1139
1140    // START BadgeSpan.BadgeSpanDimensions override
1141
1142    @Override
1143    public int getHorizontalPadding() {
1144        return sBadgePaddingExtraWidth;
1145    }
1146
1147    @Override
1148    public float getRoundedCornerRadius() {
1149        return sBadgeRoundedCornerRadius;
1150    }
1151
1152    // END BadgeSpan.BadgeSpanDimensions override
1153
1154    private boolean showActivatedText() {
1155        // For activated elements in tablet in conversation mode, we show an activated color, since
1156        // the background is dark blue for activated versus gray for non-activated.
1157        return mTabletDevice && !mListCollapsible;
1158    }
1159
1160    private boolean canFitFragment(int width, int line, int fixedWidth) {
1161        if (line == mCoordinates.sendersLineCount) {
1162            return width + fixedWidth <= mSendersWidth;
1163        } else {
1164            return width <= mSendersWidth;
1165        }
1166    }
1167
1168    private void calculateCoordinates() {
1169        startTimer(PERF_TAG_CALCULATE_COORDINATES);
1170
1171        sPaint.setTextSize(mCoordinates.dateFontSize);
1172        sPaint.setTypeface(Typeface.DEFAULT);
1173
1174        final boolean isRtl = ViewUtils.isViewRtl(this);
1175
1176        mDateWidth = (int) sPaint.measureText(
1177                mHeader.dateText != null ? mHeader.dateText.toString() : "");
1178        if (mHeader.infoIcon != null) {
1179            mInfoIconX = (isRtl) ? mCoordinates.infoIconX :
1180                    mCoordinates.infoIconXRight - mHeader.infoIcon.getWidth();
1181
1182            // If we have an info icon, we start drawing the date text:
1183            // At the end of the date TextView minus the width of the date text
1184            // In RTL mode, we just use dateX
1185            mDateX = (isRtl) ? mCoordinates.dateX : mCoordinates.dateXRight - mDateWidth;
1186        } else {
1187            // If there is no info icon, we start drawing the date text:
1188            // At the end of the info icon ImageView minus the width of the date text
1189            // We use the info icon ImageView for positioning, since we want the date text to be
1190            // at the right, since there is no info icon
1191            // In RTL, we just use infoIconX
1192            mDateX = (isRtl) ? mCoordinates.infoIconX :
1193                    mCoordinates.infoIconXRight - mDateWidth;
1194        }
1195
1196        // The paperclip is drawn starting at the start of the date text minus
1197        // the width of the paperclip and the date padding.
1198        // In RTL mode, it is at the end of the date (mDateX + mDateWidth) plus the
1199        // start date padding.
1200        mPaperclipX = (isRtl) ? mDateX + mDateWidth + mCoordinates.datePaddingStart :
1201                mDateX - ATTACHMENT.getWidth() - mCoordinates.datePaddingStart;
1202
1203        if (mCoordinates.isWideMode()) {
1204            // In wide mode, the end of the senders should align with
1205            // the start of the subject and is based on a max width.
1206            mSendersWidth = mCoordinates.sendersWidth;
1207            mSendersX = mCoordinates.sendersX;
1208        } else {
1209            // In normal mode, the senders x and width is based
1210            // on where the date/attachment icon start.
1211            final int dateAttachmentStart;
1212            // Have this end near the paperclip or date, not the folders.
1213            if (mHeader.paperclip != null) {
1214                // If there is a paperclip, the date/attachment start is at the start
1215                // of the paperclip minus the paperclip padding.
1216                // In RTL, it is at the end of the paperclip plus the paperclip padding.
1217                dateAttachmentStart = (isRtl) ?
1218                        mPaperclipX + ATTACHMENT.getWidth() + mCoordinates.paperclipPaddingStart
1219                        : mPaperclipX - mCoordinates.paperclipPaddingStart;
1220            } else {
1221                // If no paperclip, just use the start of the date minus the date padding start.
1222                // In RTL mode, this is just the paperclipX.
1223                dateAttachmentStart = (isRtl) ?
1224                        mPaperclipX : mDateX - mCoordinates.datePaddingStart;
1225            }
1226            // Senders width is the dateAttachmentStart - sendersX.
1227            // In RTL, it is sendersWidth + sendersX - dateAttachmentStart.
1228            mSendersWidth = (isRtl) ?
1229                    mCoordinates.sendersWidth + mCoordinates.sendersX - dateAttachmentStart
1230                    : dateAttachmentStart - mCoordinates.sendersX;
1231            mSendersX = (isRtl) ? dateAttachmentStart : mCoordinates.sendersX;
1232        }
1233
1234        // Second pass to layout each fragment.
1235        sPaint.setTextSize(mCoordinates.sendersFontSize);
1236        sPaint.setTypeface(Typeface.DEFAULT);
1237
1238        if (mHeader.styledNames != null) {
1239            final SpannableStringBuilder participantText = elideParticipants(mHeader.styledNames);
1240            layoutParticipantText(participantText);
1241        } else {
1242            // First pass to calculate width of each fragment.
1243            if (mSendersWidth < 0) {
1244                mSendersWidth = 0;
1245            }
1246
1247            mHeader.sendersDisplayLayout = new StaticLayout(mHeader.sendersDisplayText, sPaint,
1248                    mSendersWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
1249        }
1250
1251        if (mSendersWidth < 0) {
1252            mSendersWidth = 0;
1253        }
1254
1255        pauseTimer(PERF_TAG_CALCULATE_COORDINATES);
1256    }
1257
1258    // The rules for displaying elided participants are as follows:
1259    // 1) If there is message info (either a COUNT or DRAFT info to display), it MUST be shown
1260    // 2) If senders do not fit, ellipsize the last one that does fit, and stop
1261    // appending new senders
1262    SpannableStringBuilder elideParticipants(List<SpannableString> parts) {
1263        final SpannableStringBuilder builder = new SpannableStringBuilder();
1264        float totalWidth = 0;
1265        boolean ellipsize = false;
1266        float width;
1267        boolean skipToHeader = false;
1268
1269        // start with "To: " if we're showing recipients
1270        if (mDisplayedFolder.shouldShowRecipients() && !parts.isEmpty()) {
1271            final SpannableString toHeader = SendersView.getFormattedToHeader();
1272            CharacterStyle[] spans = toHeader.getSpans(0, toHeader.length(),
1273                    CharacterStyle.class);
1274            // There is only 1 character style span; make sure we apply all the
1275            // styles to the paint object before measuring.
1276            if (spans.length > 0) {
1277                spans[0].updateDrawState(sPaint);
1278            }
1279            totalWidth += sPaint.measureText(toHeader.toString());
1280            builder.append(toHeader);
1281            skipToHeader = true;
1282        }
1283
1284        final SpannableStringBuilder messageInfoString = mHeader.messageInfoString;
1285        if (messageInfoString.length() > 0) {
1286            CharacterStyle[] spans = messageInfoString.getSpans(0, messageInfoString.length(),
1287                    CharacterStyle.class);
1288            // There is only 1 character style span; make sure we apply all the
1289            // styles to the paint object before measuring.
1290            if (spans.length > 0) {
1291                spans[0].updateDrawState(sPaint);
1292            }
1293            // Paint the message info string to see if we lose space.
1294            float messageInfoWidth = sPaint.measureText(messageInfoString.toString());
1295            totalWidth += messageInfoWidth;
1296        }
1297       SpannableString prevSender = null;
1298       SpannableString ellipsizedText;
1299        for (SpannableString sender : parts) {
1300            // There may be null sender strings if there were dupes we had to remove.
1301            if (sender == null) {
1302                continue;
1303            }
1304            // No more width available, we'll only show fixed fragments.
1305            if (ellipsize) {
1306                break;
1307            }
1308            CharacterStyle[] spans = sender.getSpans(0, sender.length(), CharacterStyle.class);
1309            // There is only 1 character style span.
1310            if (spans.length > 0) {
1311                spans[0].updateDrawState(sPaint);
1312            }
1313            // If there are already senders present in this string, we need to
1314            // make sure we prepend the dividing token
1315            if (SendersView.sElidedString.equals(sender.toString())) {
1316                prevSender = sender;
1317                sender = copyStyles(spans, sElidedPaddingToken + sender + sElidedPaddingToken);
1318            } else if (!skipToHeader && builder.length() > 0
1319                    && (prevSender == null || !SendersView.sElidedString.equals(prevSender
1320                            .toString()))) {
1321                prevSender = sender;
1322                sender = copyStyles(spans, sSendersSplitToken + sender);
1323            } else {
1324                prevSender = sender;
1325                skipToHeader = false;
1326            }
1327            if (spans.length > 0) {
1328                spans[0].updateDrawState(sPaint);
1329            }
1330            // Measure the width of the current sender and make sure we have space
1331            width = (int) sPaint.measureText(sender.toString());
1332            if (width + totalWidth > mSendersWidth) {
1333                // The text is too long, new line won't help. We have to
1334                // ellipsize text.
1335                ellipsize = true;
1336                width = mSendersWidth - totalWidth; // ellipsis width?
1337                ellipsizedText = copyStyles(spans,
1338                        TextUtils.ellipsize(sender, sPaint, width, TruncateAt.END));
1339                width = (int) sPaint.measureText(ellipsizedText.toString());
1340            } else {
1341                ellipsizedText = null;
1342            }
1343            totalWidth += width;
1344
1345            final CharSequence fragmentDisplayText;
1346            if (ellipsizedText != null) {
1347                fragmentDisplayText = ellipsizedText;
1348            } else {
1349                fragmentDisplayText = sender;
1350            }
1351            builder.append(fragmentDisplayText);
1352        }
1353        mHeader.styledMessageInfoStringOffset = builder.length();
1354        builder.append(messageInfoString);
1355        return builder;
1356    }
1357
1358    private static SpannableString copyStyles(CharacterStyle[] spans, CharSequence newText) {
1359        SpannableString s = new SpannableString(newText);
1360        if (spans != null && spans.length > 0) {
1361            s.setSpan(spans[0], 0, s.length(), 0);
1362        }
1363        return s;
1364    }
1365
1366    /**
1367     * If the subject contains the tag of a mailing-list (text surrounded with
1368     * []), return the subject with that tag ellipsized, e.g.
1369     * "[android-gmail-team] Hello" -> "[andr...] Hello"
1370     */
1371    private String filterTag(String subject) {
1372        String result = subject;
1373        String formatString = getContext().getResources().getString(R.string.filtered_tag);
1374        if (!TextUtils.isEmpty(subject) && subject.charAt(0) == '[') {
1375            int end = subject.indexOf(']');
1376            if (end > 0) {
1377                String tag = subject.substring(1, end);
1378                result = String.format(formatString, Utils.ellipsize(tag, 7),
1379                        subject.substring(end + 1));
1380            }
1381        }
1382        return result;
1383    }
1384
1385    @Override
1386    public final void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
1387            int totalItemCount) {
1388        if (SwipeableListView.ENABLE_ATTACHMENT_PARALLAX) {
1389            if (mHeader == null || mCoordinates == null || !isAttachmentPreviewsEnabled()) {
1390                return;
1391            }
1392
1393            invalidate(mCoordinates.attachmentPreviewsX, mCoordinates.attachmentPreviewsY,
1394                    mCoordinates.attachmentPreviewsX + mCoordinates.attachmentPreviewsWidth,
1395                    mCoordinates.attachmentPreviewsY + mCoordinates.attachmentPreviewsHeight);
1396        }
1397    }
1398
1399    @Override
1400    public void onScrollStateChanged(AbsListView view, int scrollState) {
1401    }
1402
1403    @Override
1404    protected void onDraw(Canvas canvas) {
1405        Utils.traceBeginSection("CIVC.draw");
1406
1407        // Contact photo
1408        if (mGadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO) {
1409            canvas.save();
1410            Utils.traceBeginSection("draw senders image");
1411            drawSendersImage(canvas);
1412            Utils.traceEndSection();
1413            canvas.restore();
1414        }
1415
1416        // Senders.
1417        boolean isUnread = mHeader.unread;
1418        // Old style senders; apply text colors/ sizes/ styling.
1419        canvas.save();
1420        if (mHeader.sendersDisplayLayout != null) {
1421            sPaint.setTextSize(mCoordinates.sendersFontSize);
1422            sPaint.setTypeface(SendersView.getTypeface(isUnread));
1423            sPaint.setColor(isUnread ? sSendersTextColorUnread : sSendersTextColorRead);
1424            canvas.translate(mSendersX, mCoordinates.sendersY
1425                    + mHeader.sendersDisplayLayout.getTopPadding());
1426            mHeader.sendersDisplayLayout.draw(canvas);
1427        } else {
1428            drawSenders(canvas);
1429        }
1430        canvas.restore();
1431
1432
1433        // Subject.
1434        sPaint.setTypeface(Typeface.DEFAULT);
1435        canvas.save();
1436        drawSubject(canvas);
1437        canvas.restore();
1438
1439        if (mCoordinates.isWideMode()) {
1440            canvas.save();
1441            drawBadge(canvas);
1442            canvas.restore();
1443        }
1444
1445        // Folders.
1446        if (mConfig.areFoldersVisible()) {
1447            mHeader.folderDisplayer.drawFolders(canvas, mCoordinates, ViewUtils.isViewRtl(this));
1448        }
1449
1450        // If this folder has a color (combined view/Email), show it here
1451        if (mConfig.isColorBlockVisible()) {
1452            sFoldersPaint.setColor(mHeader.conversation.color);
1453            sFoldersPaint.setStyle(Paint.Style.FILL);
1454            canvas.drawRect(mCoordinates.colorBlockX, mCoordinates.colorBlockY,
1455                    mCoordinates.colorBlockX + mCoordinates.colorBlockWidth,
1456                    mCoordinates.colorBlockY + mCoordinates.colorBlockHeight, sFoldersPaint);
1457        }
1458
1459        // Draw the reply state. Draw nothing if neither replied nor forwarded.
1460        if (mConfig.isReplyStateVisible()) {
1461            if (mHeader.hasBeenRepliedTo && mHeader.hasBeenForwarded) {
1462                canvas.drawBitmap(STATE_REPLIED_AND_FORWARDED, mCoordinates.replyStateX,
1463                        mCoordinates.replyStateY, null);
1464            } else if (mHeader.hasBeenRepliedTo) {
1465                canvas.drawBitmap(STATE_REPLIED, mCoordinates.replyStateX,
1466                        mCoordinates.replyStateY, null);
1467            } else if (mHeader.hasBeenForwarded) {
1468                canvas.drawBitmap(STATE_FORWARDED, mCoordinates.replyStateX,
1469                        mCoordinates.replyStateY, null);
1470            } else if (mHeader.isInvite) {
1471                canvas.drawBitmap(STATE_CALENDAR_INVITE, mCoordinates.replyStateX,
1472                        mCoordinates.replyStateY, null);
1473            }
1474        }
1475
1476        if (mConfig.isPersonalIndicatorVisible()) {
1477            canvas.drawBitmap(mHeader.personalLevelBitmap, mCoordinates.personalIndicatorX,
1478                    mCoordinates.personalIndicatorY, null);
1479        }
1480
1481        // Info icon
1482        if (mHeader.infoIcon != null) {
1483            canvas.drawBitmap(mHeader.infoIcon, mInfoIconX, mCoordinates.infoIconY, sPaint);
1484        }
1485
1486        // Date.
1487        sPaint.setTextSize(mCoordinates.dateFontSize);
1488        sPaint.setTypeface(Typeface.DEFAULT);
1489        sPaint.setColor(sDateTextColor);
1490        drawText(canvas, mHeader.dateText, mDateX, mCoordinates.dateYBaseline,
1491                sPaint);
1492
1493        // Paper clip icon.
1494        if (mHeader.paperclip != null) {
1495            canvas.drawBitmap(mHeader.paperclip, mPaperclipX, mCoordinates.paperclipY, sPaint);
1496        }
1497
1498        if (mStarEnabled) {
1499            // Star.
1500            canvas.drawBitmap(getStarBitmap(), mCoordinates.starX, mCoordinates.starY, sPaint);
1501        }
1502
1503        // Attachment previews
1504        if (isAttachmentPreviewsEnabled()) {
1505            canvas.save();
1506            drawAttachmentPreviews(canvas);
1507            canvas.restore();
1508        }
1509
1510        // right-side edge effect when in tablet conversation mode and the list is not collapsed
1511        if (Utils.getDisplayListRightEdgeEffect(mTabletDevice, mListCollapsible,
1512                mConfig.getViewMode())) {
1513            final boolean isRtl = ViewUtils.isViewRtl(this);
1514            RIGHT_EDGE_TABLET.setBounds(
1515                    (isRtl) ? 0 : getWidth() - RIGHT_EDGE_TABLET.getIntrinsicWidth(), 0,
1516                    (isRtl) ? RIGHT_EDGE_TABLET.getIntrinsicWidth() : getWidth(), getHeight());
1517            RIGHT_EDGE_TABLET.draw(canvas);
1518
1519            if (isActivated()) {
1520                // draw caret on the end, centered vertically
1521                final int x = (isRtl) ? 0 : getWidth() - VISIBLE_CONVERSATION_CARET.getWidth();
1522                final int y = (getHeight() - VISIBLE_CONVERSATION_CARET.getHeight()) / 2;
1523                if (isRtl) {
1524                    // draw the bitmap mirrored in RTL mode
1525                    canvas.save();
1526                    canvas.scale(-1, 1,
1527                            x + VISIBLE_CONVERSATION_CARET.getWidth()/2,
1528                            y + VISIBLE_CONVERSATION_CARET.getHeight()/2);
1529                    canvas.drawBitmap(VISIBLE_CONVERSATION_CARET, x, y, null);
1530                    canvas.restore();
1531                } else {
1532                    canvas.drawBitmap(VISIBLE_CONVERSATION_CARET, x, y, null);
1533                }
1534            }
1535        }
1536        Utils.traceEndSection();
1537    }
1538
1539    private void drawSendersImage(final Canvas canvas) {
1540        if (!mSendersImageView.isFlipping()) {
1541            final boolean showSenders = !isSelected();
1542            mSendersImageView.reset(showSenders);
1543        }
1544        canvas.translate(mCoordinates.contactImagesX, mCoordinates.contactImagesY);
1545        if (mPhotoBitmap == null) {
1546            mSendersImageView.draw(canvas);
1547        } else {
1548            canvas.drawBitmap(mPhotoBitmap, null, mPhotoRect, sPaint);
1549        }
1550    }
1551
1552    private void drawAttachmentPreviews(Canvas canvas) {
1553        canvas.translate(mCoordinates.attachmentPreviewsX, mCoordinates.attachmentPreviewsY);
1554        final float fraction;
1555        if (SwipeableListView.ENABLE_ATTACHMENT_PARALLAX) {
1556            final View listView = getListView();
1557            final View listItemView = unwrap();
1558            if (mParallaxDirectionAlternative) {
1559                fraction = 1 - (float) listItemView.getBottom()
1560                        / (listView.getHeight() + listItemView.getHeight());
1561            } else {
1562                fraction = (float) listItemView.getBottom()
1563                        / (listView.getHeight() + listItemView.getHeight());
1564            }
1565        } else {
1566            // Vertically center the preview crop, which has already been decoded at 1/3.
1567            fraction = 0.5f;
1568        }
1569        mAttachmentsView.setParallaxFraction(fraction);
1570        mAttachmentsView.draw(canvas);
1571    }
1572
1573    private void drawSubject(Canvas canvas) {
1574        canvas.translate(mCoordinates.subjectX, mCoordinates.subjectY);
1575        mSubjectTextView.draw(canvas);
1576    }
1577
1578    private void drawBadge(Canvas canvas) {
1579        canvas.translate(mCoordinates.badgeLeft, mCoordinates.badgeTop);
1580        mBadgeTextView.draw(canvas);
1581    }
1582
1583    private void drawSenders(Canvas canvas) {
1584        canvas.translate(mSendersX, mCoordinates.sendersY);
1585        mSendersTextView.draw(canvas);
1586    }
1587
1588    private Bitmap getStarBitmap() {
1589        return mHeader.conversation.starred ? STAR_ON : STAR_OFF;
1590    }
1591
1592    private static void drawText(Canvas canvas, CharSequence s, int x, int y, TextPaint paint) {
1593        canvas.drawText(s, 0, s.length(), x, y, paint);
1594    }
1595
1596    /**
1597     * Set the background for this item based on:
1598     * 1. Read / Unread (unread messages have a lighter background)
1599     * 2. Tablet / Phone
1600     * 3. Checkbox checked / Unchecked (controls CAB color for item)
1601     * 4. Activated / Not activated (controls the blue highlight on tablet)
1602     */
1603    private void updateBackground() {
1604        final int background;
1605        if (mBackgroundOverrideResId > 0) {
1606            background = mBackgroundOverrideResId;
1607        } else if (mHeader.unread) {
1608            background = R.drawable.conversation_unread_selector;
1609        } else {
1610            background = R.drawable.conversation_read_selector;
1611        }
1612        setBackgroundResource(background);
1613    }
1614
1615    /**
1616     * Toggle the check mark on this view and update the conversation or begin
1617     * drag, if drag is enabled.
1618     */
1619    @Override
1620    public boolean toggleSelectedStateOrBeginDrag() {
1621        ViewMode mode = mActivity.getViewMode();
1622        if (mIsExpansiveTablet && mode.isListMode()) {
1623            return beginDragMode();
1624        } else {
1625            return toggleSelectedState("long_press");
1626        }
1627    }
1628
1629    @Override
1630    public boolean toggleSelectedState() {
1631        return toggleSelectedState(null);
1632    }
1633
1634    private boolean toggleSelectedState(final String sourceOpt) {
1635        if (mHeader != null && mHeader.conversation != null && mSelectedConversationSet != null) {
1636            mSelected = !mSelected;
1637            setSelected(mSelected);
1638            final Conversation conv = mHeader.conversation;
1639            // Set the list position of this item in the conversation
1640            final SwipeableListView listView = getListView();
1641
1642            try {
1643                conv.position = mSelected && listView != null ? listView.getPositionForView(this)
1644                        : Conversation.NO_POSITION;
1645            } catch (final NullPointerException e) {
1646                // TODO(skennedy) Remove this if we find the root cause b/9527863
1647            }
1648
1649            if (mSelectedConversationSet.isEmpty()) {
1650                final String source = (sourceOpt != null) ? sourceOpt : "checkbox";
1651                Analytics.getInstance().sendEvent("enter_cab_mode", source, null, 0);
1652            }
1653
1654            mSelectedConversationSet.toggle(conv);
1655            if (mSelectedConversationSet.isEmpty()) {
1656                listView.commitDestructiveActions(true);
1657            }
1658
1659            final boolean front = !mSelected;
1660            mSendersImageView.flipTo(front);
1661
1662            // We update the background after the checked state has changed
1663            // now that we have a selected background asset. Setting the background
1664            // usually waits for a layout pass, but we don't need a full layout,
1665            // just an update to the background.
1666            requestLayout();
1667
1668            return true;
1669        }
1670
1671        return false;
1672    }
1673
1674    @Override
1675    public void onSetEmpty() {
1676        mSendersImageView.flipTo(true);
1677    }
1678
1679    @Override
1680    public void onSetPopulated(final ConversationSelectionSet set) { }
1681
1682    @Override
1683    public void onSetChanged(final ConversationSelectionSet set) { }
1684
1685    /**
1686     * Toggle the star on this view and update the conversation.
1687     */
1688    public void toggleStar() {
1689        mHeader.conversation.starred = !mHeader.conversation.starred;
1690        Bitmap starBitmap = getStarBitmap();
1691        postInvalidate(mCoordinates.starX, mCoordinates.starY, mCoordinates.starX
1692                + starBitmap.getWidth(),
1693                mCoordinates.starY + starBitmap.getHeight());
1694        ConversationCursor cursor = (ConversationCursor) mAdapter.getCursor();
1695        if (cursor != null) {
1696            // TODO(skennedy) What about ads?
1697            cursor.updateBoolean(mHeader.conversation, ConversationColumns.STARRED,
1698                    mHeader.conversation.starred);
1699        }
1700    }
1701
1702    private boolean isTouchInContactPhoto(float x, float y) {
1703        // Everything before the end edge of contact photo
1704
1705        final boolean isRtl = ViewUtils.isViewRtl(this);
1706        final int threshold = (isRtl) ? mCoordinates.contactImagesX - sSenderImageTouchSlop :
1707                mCoordinates.contactImagesX + mCoordinates.contactImagesWidth
1708                + sSenderImageTouchSlop;
1709
1710        // Allow touching a little right of the contact photo when we're already in selection mode
1711        final float extra;
1712        if (mSelectedConversationSet == null || mSelectedConversationSet.isEmpty()) {
1713            extra = 0;
1714        } else {
1715            extra = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16,
1716                    getResources().getDisplayMetrics());
1717        }
1718
1719        return mHeader.gadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO
1720                && ((isRtl) ? x > (threshold - extra) : x < (threshold + extra))
1721                && (!isAttachmentPreviewsEnabled() || y < mCoordinates.attachmentPreviewsY);
1722    }
1723
1724    private boolean isTouchInInfoIcon(final float x, final float y) {
1725        if (mHeader.infoIcon == null) {
1726            // We have no info icon
1727            return false;
1728        }
1729
1730        final boolean isRtl = ViewUtils.isViewRtl(this);
1731        // Regardless of device, we always want to be end of the date's start touch slop
1732        if (((isRtl) ? x > mDateX + mDateWidth + sStarTouchSlop : x < mDateX - sStarTouchSlop)) {
1733            return false;
1734        }
1735
1736        if (mStarEnabled) {
1737            if (mCoordinates.isWideMode()) {
1738                // Just check that we're to start of the star's touch area
1739                if (isTouchInStarTargetX(isRtl, x)) {
1740                    return false;
1741                }
1742            } else {
1743                // We're on a single pane device with the more condensed layout
1744
1745                // We allow touches all the way to the right edge, so no x check is necessary
1746
1747                // We need to be above the star's touch area, which ends at the top of the subject
1748                // text
1749                return y < mCoordinates.subjectY;
1750            }
1751        }
1752
1753        // With no star below the info icon, we allow touches anywhere from the top edge to the
1754        // bottom edge, or to the top of the attachment previews, whichever is higher
1755        return !isAttachmentPreviewsEnabled() || y < mCoordinates.attachmentPreviewsY;
1756    }
1757
1758    private boolean isTouchInStar(float x, float y) {
1759        if (mHeader.infoIcon != null
1760                && !mCoordinates.isWideMode()) {
1761            // We have an info icon, and it's above the star
1762            // We allow touches everywhere below the top of the subject text
1763            if (y < mCoordinates.subjectY) {
1764                return false;
1765            }
1766        }
1767
1768        // Everything after the star and include a touch slop.
1769        return mStarEnabled
1770                && isTouchInStarTargetX(ViewUtils.isViewRtl(this), x)
1771                && (!isAttachmentPreviewsEnabled() || y < mCoordinates.attachmentPreviewsY);
1772    }
1773
1774    private boolean isTouchInStarTargetX(boolean isRtl, float x) {
1775        return (isRtl) ? x < mCoordinates.starX + mCoordinates.starWidth + sStarTouchSlop
1776                : x >= mCoordinates.starX - sStarTouchSlop;
1777    }
1778
1779    @Override
1780    public boolean canChildBeDismissed() {
1781        return true;
1782    }
1783
1784    @Override
1785    public void dismiss() {
1786        SwipeableListView listView = getListView();
1787        if (listView != null) {
1788            listView.dismissChild(this);
1789        }
1790    }
1791
1792    private boolean onTouchEventNoSwipe(MotionEvent event) {
1793        Utils.traceBeginSection("on touch event no swipe");
1794        boolean handled = false;
1795
1796        int x = (int) event.getX();
1797        int y = (int) event.getY();
1798        mLastTouchX = x;
1799        mLastTouchY = y;
1800        switch (event.getAction()) {
1801            case MotionEvent.ACTION_DOWN:
1802                if (isTouchInContactPhoto(x, y) || isTouchInInfoIcon(x, y) || isTouchInStar(x, y)) {
1803                    mDownEvent = true;
1804                    handled = true;
1805                }
1806                break;
1807
1808            case MotionEvent.ACTION_CANCEL:
1809                mDownEvent = false;
1810                break;
1811
1812            case MotionEvent.ACTION_UP:
1813                if (mDownEvent) {
1814                    if (isTouchInContactPhoto(x, y)) {
1815                        // Touch on the check mark
1816                        toggleSelectedState();
1817                    } else if (isTouchInInfoIcon(x, y)) {
1818                        if (mConversationItemAreaClickListener != null) {
1819                            mConversationItemAreaClickListener.onInfoIconClicked();
1820                        }
1821                    } else if (isTouchInStar(x, y)) {
1822                        // Touch on the star
1823                        if (mConversationItemAreaClickListener == null) {
1824                            toggleStar();
1825                        } else {
1826                            mConversationItemAreaClickListener.onStarClicked();
1827                        }
1828                    }
1829                    handled = true;
1830                }
1831                break;
1832        }
1833
1834        if (!handled) {
1835            handled = super.onTouchEvent(event);
1836        }
1837
1838        Utils.traceEndSection();
1839        return handled;
1840    }
1841
1842    /**
1843     * ConversationItemView is given the first chance to handle touch events.
1844     */
1845    @Override
1846    public boolean onTouchEvent(MotionEvent event) {
1847        Utils.traceBeginSection("on touch event");
1848        int x = (int) event.getX();
1849        int y = (int) event.getY();
1850        mLastTouchX = x;
1851        mLastTouchY = y;
1852        if (!mSwipeEnabled) {
1853            Utils.traceEndSection();
1854            return onTouchEventNoSwipe(event);
1855        }
1856        switch (event.getAction()) {
1857            case MotionEvent.ACTION_DOWN:
1858                if (isTouchInContactPhoto(x, y) || isTouchInInfoIcon(x, y) || isTouchInStar(x, y)) {
1859                    mDownEvent = true;
1860                    Utils.traceEndSection();
1861                    return true;
1862                }
1863                break;
1864            case MotionEvent.ACTION_UP:
1865                if (mDownEvent) {
1866                    if (isTouchInContactPhoto(x, y)) {
1867                        // Touch on the check mark
1868                        Utils.traceEndSection();
1869                        mDownEvent = false;
1870                        toggleSelectedState();
1871                        Utils.traceEndSection();
1872                        return true;
1873                    } else if (isTouchInInfoIcon(x, y)) {
1874                        // Touch on the info icon
1875                        mDownEvent = false;
1876                        if (mConversationItemAreaClickListener != null) {
1877                            mConversationItemAreaClickListener.onInfoIconClicked();
1878                        }
1879                        Utils.traceEndSection();
1880                        return true;
1881                    } else if (isTouchInStar(x, y)) {
1882                        // Touch on the star
1883                        mDownEvent = false;
1884                        if (mConversationItemAreaClickListener == null) {
1885                            toggleStar();
1886                        } else {
1887                            mConversationItemAreaClickListener.onStarClicked();
1888                        }
1889                        Utils.traceEndSection();
1890                        return true;
1891                    }
1892                }
1893                break;
1894        }
1895        // Let View try to handle it as well.
1896        boolean handled = super.onTouchEvent(event);
1897        if (event.getAction() == MotionEvent.ACTION_DOWN) {
1898            Utils.traceEndSection();
1899            return true;
1900        }
1901        Utils.traceEndSection();
1902        return handled;
1903    }
1904
1905    @Override
1906    public boolean performClick() {
1907        final boolean handled = super.performClick();
1908        final SwipeableListView list = getListView();
1909        if (!handled && list != null && list.getAdapter() != null) {
1910            final int pos = list.findConversation(this, mHeader.conversation);
1911            list.performItemClick(this, pos, mHeader.conversation.id);
1912        }
1913        return handled;
1914    }
1915
1916    private View unwrap() {
1917        final ViewParent vp = getParent();
1918        if (vp == null || !(vp instanceof View)) {
1919            return null;
1920        }
1921        return (View) vp;
1922    }
1923
1924    private SwipeableListView getListView() {
1925        SwipeableListView v = null;
1926        final View wrapper = unwrap();
1927        if (wrapper != null && wrapper instanceof SwipeableConversationItemView) {
1928            v = (SwipeableListView) ((SwipeableConversationItemView) wrapper).getListView();
1929        }
1930        if (v == null) {
1931            v = mAdapter.getListView();
1932        }
1933        return v;
1934    }
1935
1936    /**
1937     * Reset any state associated with this conversation item view so that it
1938     * can be reused.
1939     */
1940    public void reset() {
1941        Utils.traceBeginSection("reset");
1942        setAlpha(1f);
1943        setTranslationX(0f);
1944        mAnimatedHeightFraction = 1.0f;
1945        Utils.traceEndSection();
1946    }
1947
1948    @SuppressWarnings("deprecation")
1949    @Override
1950    public void setTranslationX(float translationX) {
1951        super.setTranslationX(translationX);
1952
1953        // When a list item is being swiped or animated, ensure that the hosting view has a
1954        // background color set. We only enable the background during the X-translation effect to
1955        // reduce overdraw during normal list scrolling.
1956        final View parent = (View) getParent();
1957        if (parent == null) {
1958            LogUtils.w(LOG_TAG, "CIV.setTranslationX null ConversationItemView parent x=%s",
1959                    translationX);
1960        }
1961
1962        if (parent instanceof SwipeableConversationItemView) {
1963            if (translationX != 0f) {
1964                parent.setBackgroundResource(R.color.swiped_bg_color);
1965            } else {
1966                parent.setBackgroundDrawable(null);
1967            }
1968        }
1969    }
1970
1971    /**
1972     * Grow the height of the item and fade it in when bringing a conversation
1973     * back from a destructive action.
1974     */
1975    public Animator createSwipeUndoAnimation() {
1976        ObjectAnimator undoAnimator = createTranslateXAnimation(true);
1977        return undoAnimator;
1978    }
1979
1980    /**
1981     * Grow the height of the item and fade it in when bringing a conversation
1982     * back from a destructive action.
1983     */
1984    public Animator createUndoAnimation() {
1985        ObjectAnimator height = createHeightAnimation(true);
1986        Animator fade = ObjectAnimator.ofFloat(this, "alpha", 0, 1.0f);
1987        fade.setDuration(sShrinkAnimationDuration);
1988        fade.setInterpolator(new DecelerateInterpolator(2.0f));
1989        AnimatorSet transitionSet = new AnimatorSet();
1990        transitionSet.playTogether(height, fade);
1991        transitionSet.addListener(new HardwareLayerEnabler(this));
1992        return transitionSet;
1993    }
1994
1995    /**
1996     * Grow the height of the item and fade it in when bringing a conversation
1997     * back from a destructive action.
1998     */
1999    public Animator createDestroyWithSwipeAnimation() {
2000        ObjectAnimator slide = createTranslateXAnimation(false);
2001        ObjectAnimator height = createHeightAnimation(false);
2002        AnimatorSet transitionSet = new AnimatorSet();
2003        transitionSet.playSequentially(slide, height);
2004        return transitionSet;
2005    }
2006
2007    private ObjectAnimator createTranslateXAnimation(boolean show) {
2008        SwipeableListView parent = getListView();
2009        // If we can't get the parent...we have bigger problems.
2010        int width = parent != null ? parent.getMeasuredWidth() : 0;
2011        final float start = show ? width : 0f;
2012        final float end = show ? 0f : width;
2013        ObjectAnimator slide = ObjectAnimator.ofFloat(this, "translationX", start, end);
2014        slide.setInterpolator(new DecelerateInterpolator(2.0f));
2015        slide.setDuration(sSlideAnimationDuration);
2016        return slide;
2017    }
2018
2019    public Animator createDestroyAnimation() {
2020        return createHeightAnimation(false);
2021    }
2022
2023    private ObjectAnimator createHeightAnimation(boolean show) {
2024        final float start = show ? 0f : 1.0f;
2025        final float end = show ? 1.0f : 0f;
2026        ObjectAnimator height = ObjectAnimator.ofFloat(this, "animatedHeightFraction", start, end);
2027        height.setInterpolator(new DecelerateInterpolator(2.0f));
2028        height.setDuration(sShrinkAnimationDuration);
2029        return height;
2030    }
2031
2032    // Used by animator
2033    public void setAnimatedHeightFraction(float height) {
2034        mAnimatedHeightFraction = height;
2035        requestLayout();
2036    }
2037
2038    @Override
2039    public SwipeableView getSwipeableView() {
2040        return SwipeableView.from(this);
2041    }
2042
2043    /**
2044     * Begin drag mode. Keep the conversation selected (NOT toggle selection) and start drag.
2045     */
2046    private boolean beginDragMode() {
2047        if (mLastTouchX < 0 || mLastTouchY < 0 ||  mSelectedConversationSet == null) {
2048            return false;
2049        }
2050        // If this is already checked, don't bother unchecking it!
2051        if (!mSelected) {
2052            toggleSelectedState();
2053        }
2054
2055        // Clip data has form: [conversations_uri, conversationId1,
2056        // maxMessageId1, label1, conversationId2, maxMessageId2, label2, ...]
2057        final int count = mSelectedConversationSet.size();
2058        String description = Utils.formatPlural(mContext, R.plurals.move_conversation, count);
2059
2060        final ClipData data = ClipData.newUri(mContext.getContentResolver(), description,
2061                Conversation.MOVE_CONVERSATIONS_URI);
2062        for (Conversation conversation : mSelectedConversationSet.values()) {
2063            data.addItem(new Item(String.valueOf(conversation.position)));
2064        }
2065        // Protect against non-existent views: only happens for monkeys
2066        final int width = this.getWidth();
2067        final int height = this.getHeight();
2068        final boolean isDimensionNegative = (width < 0) || (height < 0);
2069        if (isDimensionNegative) {
2070            LogUtils.e(LOG_TAG, "ConversationItemView: dimension is negative: "
2071                        + "width=%d, height=%d", width, height);
2072            return false;
2073        }
2074        mActivity.startDragMode();
2075        // Start drag mode
2076        startDrag(data, new ShadowBuilder(this, count, mLastTouchX, mLastTouchY), null, 0);
2077
2078        return true;
2079    }
2080
2081    /**
2082     * Handles the drag event.
2083     *
2084     * @param event the drag event to be handled
2085     */
2086    @Override
2087    public boolean onDragEvent(DragEvent event) {
2088        switch (event.getAction()) {
2089            case DragEvent.ACTION_DRAG_ENDED:
2090                mActivity.stopDragMode();
2091                return true;
2092        }
2093        return false;
2094    }
2095
2096    private class ShadowBuilder extends DragShadowBuilder {
2097        private final Drawable mBackground;
2098
2099        private final View mView;
2100        private final String mDragDesc;
2101        private final int mTouchX;
2102        private final int mTouchY;
2103        private int mDragDescX;
2104        private int mDragDescY;
2105
2106        public ShadowBuilder(View view, int count, int touchX, int touchY) {
2107            super(view);
2108            mView = view;
2109            mBackground = mView.getResources().getDrawable(R.drawable.list_pressed_holo);
2110            mDragDesc = Utils.formatPlural(mView.getContext(), R.plurals.move_conversation, count);
2111            mTouchX = touchX;
2112            mTouchY = touchY;
2113        }
2114
2115        @Override
2116        public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint) {
2117            final int width = mView.getWidth();
2118            final int height = mView.getHeight();
2119
2120            sPaint.setTextSize(mCoordinates.subjectFontSize);
2121            mDragDescX = mCoordinates.sendersX;
2122            mDragDescY = (height - (int) mCoordinates.subjectFontSize) / 2 ;
2123            shadowSize.set(width, height);
2124            shadowTouchPoint.set(mTouchX, mTouchY);
2125        }
2126
2127        @Override
2128        public void onDrawShadow(Canvas canvas) {
2129            mBackground.setBounds(0, 0, mView.getWidth(), mView.getHeight());
2130            mBackground.draw(canvas);
2131            sPaint.setTextSize(mCoordinates.subjectFontSize);
2132            canvas.drawText(mDragDesc, mDragDescX, mDragDescY - sPaint.ascent(), sPaint);
2133        }
2134    }
2135
2136    @Override
2137    public float getMinAllowScrollDistance() {
2138        return sScrollSlop;
2139    }
2140
2141    public String getAccount() {
2142        return mAccount;
2143    }
2144}
2145