• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
18 package com.android.mail.browse;
19 
20 import android.animation.Animator;
21 import android.animation.AnimatorSet;
22 import android.animation.ObjectAnimator;
23 import android.content.BroadcastReceiver;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.IntentFilter;
27 import android.content.res.Resources;
28 import android.graphics.Bitmap;
29 import android.graphics.BitmapFactory;
30 import android.graphics.Canvas;
31 import android.graphics.Color;
32 import android.graphics.Paint;
33 import android.graphics.Rect;
34 import android.graphics.Typeface;
35 import android.graphics.drawable.Drawable;
36 import android.graphics.drawable.InsetDrawable;
37 import androidx.annotation.Nullable;
38 import androidx.core.text.BidiFormatter;
39 import androidx.core.text.TextUtilsCompat;
40 import androidx.core.view.ViewCompat;
41 import android.text.Layout.Alignment;
42 import android.text.Spannable;
43 import android.text.SpannableString;
44 import android.text.SpannableStringBuilder;
45 import android.text.StaticLayout;
46 import android.text.TextPaint;
47 import android.text.TextUtils;
48 import android.text.TextUtils.TruncateAt;
49 import android.text.format.DateUtils;
50 import android.text.style.BackgroundColorSpan;
51 import android.text.style.CharacterStyle;
52 import android.text.style.ForegroundColorSpan;
53 import android.text.style.TextAppearanceSpan;
54 import android.util.SparseArray;
55 import android.util.TypedValue;
56 import android.view.MotionEvent;
57 import android.view.View;
58 import android.view.ViewGroup;
59 import android.view.ViewParent;
60 import android.view.animation.DecelerateInterpolator;
61 import android.widget.TextView;
62 
63 import com.android.mail.R;
64 import com.android.mail.analytics.Analytics;
65 import com.android.mail.bitmap.CheckableContactFlipDrawable;
66 import com.android.mail.bitmap.ContactDrawable;
67 import com.android.mail.perf.Timer;
68 import com.android.mail.providers.Account;
69 import com.android.mail.providers.Conversation;
70 import com.android.mail.providers.Folder;
71 import com.android.mail.providers.UIProvider;
72 import com.android.mail.providers.UIProvider.ConversationColumns;
73 import com.android.mail.providers.UIProvider.ConversationListIcon;
74 import com.android.mail.providers.UIProvider.FolderType;
75 import com.android.mail.ui.AnimatedAdapter;
76 import com.android.mail.ui.ControllableActivity;
77 import com.android.mail.ui.ConversationCheckedSet;
78 import com.android.mail.ui.ConversationSetObserver;
79 import com.android.mail.ui.FolderDisplayer;
80 import com.android.mail.ui.SwipeableItemView;
81 import com.android.mail.ui.SwipeableListView;
82 import com.android.mail.utils.FolderUri;
83 import com.android.mail.utils.HardwareLayerEnabler;
84 import com.android.mail.utils.LogTag;
85 import com.android.mail.utils.LogUtils;
86 import com.android.mail.utils.Utils;
87 import com.android.mail.utils.ViewUtils;
88 import com.google.common.annotations.VisibleForTesting;
89 
90 import java.util.List;
91 import java.util.Locale;
92 
93 public class ConversationItemView extends View
94         implements SwipeableItemView, ToggleableItem, ConversationSetObserver,
95         BadgeSpan.BadgeSpanDimensions {
96 
97     // Timer.
98     private static int sLayoutCount = 0;
99     private static Timer sTimer; // Create the sTimer here if you need to do
100                                  // perf analysis.
101     private static final int PERF_LAYOUT_ITERATIONS = 50;
102     private static final String PERF_TAG_LAYOUT = "CCHV.layout";
103     private static final String PERF_TAG_CALCULATE_TEXTS_BITMAPS = "CCHV.txtsbmps";
104     private static final String PERF_TAG_CALCULATE_SENDER_SUBJECT = "CCHV.sendersubj";
105     private static final String PERF_TAG_CALCULATE_FOLDERS = "CCHV.folders";
106     private static final String PERF_TAG_CALCULATE_COORDINATES = "CCHV.coordinates";
107     private static final String LOG_TAG = LogTag.getLogTag();
108 
109     private static final Typeface SANS_SERIF_BOLD = Typeface.create("sans-serif", Typeface.BOLD);
110 
111     private static final Typeface SANS_SERIF_LIGHT = Typeface.create("sans-serif-light",
112             Typeface.NORMAL);
113 
114     private static final int[] CHECKED_STATE = new int[] { android.R.attr.state_checked };
115 
116     // Static bitmaps.
117     private static Bitmap STAR_OFF;
118     private static Bitmap STAR_ON;
119     private static Bitmap ATTACHMENT;
120     private static Bitmap ONLY_TO_ME;
121     private static Bitmap TO_ME_AND_OTHERS;
122     private static Bitmap IMPORTANT_ONLY_TO_ME;
123     private static Bitmap IMPORTANT_TO_ME_AND_OTHERS;
124     private static Bitmap IMPORTANT;
125     private static Bitmap STATE_REPLIED;
126     private static Bitmap STATE_FORWARDED;
127     private static Bitmap STATE_REPLIED_AND_FORWARDED;
128     private static Bitmap STATE_CALENDAR_INVITE;
129     private static Drawable FOCUSED_CONVERSATION_HIGHLIGHT;
130 
131     private static String sSendersSplitToken;
132     private static String sElidedPaddingToken;
133 
134     // Static colors.
135     private static int sSendersTextColor;
136     private static int sDateTextColorRead;
137     private static int sDateTextColorUnread;
138     private static int sStarTouchSlop;
139     private static int sSenderImageTouchSlop;
140     private static int sShrinkAnimationDuration;
141     private static int sSlideAnimationDuration;
142     private static int sCabAnimationDuration;
143     private static int sBadgePaddingExtraWidth;
144     private static int sBadgeRoundedCornerRadius;
145 
146     // Static paints.
147     private static final TextPaint sPaint = new TextPaint();
148     private static final TextPaint sFoldersPaint = new TextPaint();
149     private static final Paint sCheckBackgroundPaint = new Paint();
150     private static final Paint sDividerPaint = new Paint();
151 
152     private static int sDividerHeight;
153 
154     private static BroadcastReceiver sConfigurationChangedReceiver;
155 
156     // Backgrounds for different states.
157     private final SparseArray<Drawable> mBackgrounds = new SparseArray<Drawable>();
158 
159     // Dimensions and coordinates.
160     private int mViewWidth = -1;
161     /** The view mode at which we calculated mViewWidth previously. */
162     private int mPreviousMode;
163 
164     private int mInfoIconX;
165     private int mDateX;
166     private int mDateWidth;
167     private int mPaperclipX;
168     private int mSendersX;
169     private int mSendersWidth;
170 
171     /** Whether we are on a tablet device or not */
172     private final boolean mTabletDevice;
173     /** When in conversation mode, true if the list is hidden */
174     private final boolean mListCollapsible;
175 
176     @VisibleForTesting
177     ConversationItemViewCoordinates mCoordinates;
178 
179     private ConversationItemViewCoordinates.Config mConfig;
180 
181     private final Context mContext;
182 
183     private ConversationItemViewModel mHeader;
184     private boolean mDownEvent;
185     private boolean mChecked = false;
186     private ConversationCheckedSet mCheckedConversationSet;
187     private Folder mDisplayedFolder;
188     private boolean mStarEnabled;
189     private boolean mSwipeEnabled;
190     private boolean mDividerEnabled;
191     private AnimatedAdapter mAdapter;
192     private float mAnimatedHeightFraction = 1.0f;
193     private final Account mAccount;
194     private ControllableActivity mActivity;
195     private final TextView mSendersTextView;
196     private final TextView mSubjectTextView;
197     private final TextView mSnippetTextView;
198     private int mGadgetMode;
199 
200     private static int sFoldersMaxCount;
201     private static TextAppearanceSpan sSubjectTextUnreadSpan;
202     private static TextAppearanceSpan sSubjectTextReadSpan;
203     private static TextAppearanceSpan sBadgeTextSpan;
204     private static BackgroundColorSpan sBadgeBackgroundSpan;
205     private static int sScrollSlop;
206     private static CharacterStyle sActivatedTextSpan;
207 
208     private final CheckableContactFlipDrawable mSendersImageView;
209 
210     /** The resource id of the color to use to override the background. */
211     private int mBackgroundOverrideResId = -1;
212     /** The bitmap to use, or <code>null</code> for the default */
213     private Bitmap mPhotoBitmap = null;
214     private Rect mPhotoRect = new Rect();
215 
216     /**
217      * A listener for clicks on the various areas of a conversation item.
218      */
219     public interface ConversationItemAreaClickListener {
220         /** Called when the info icon is clicked. */
onInfoIconClicked()221         void onInfoIconClicked();
222 
223         /** Called when the star is clicked. */
onStarClicked()224         void onStarClicked();
225     }
226 
227     /** If set, it will steal all clicks for which the interface has a click method. */
228     private ConversationItemAreaClickListener mConversationItemAreaClickListener = null;
229 
230     static {
231         sPaint.setAntiAlias(true);
232         sFoldersPaint.setAntiAlias(true);
233 
234         sCheckBackgroundPaint.setColor(Color.GRAY);
235     }
236 
237     /**
238      * Handles displaying folders in a conversation header view.
239      */
240     static class ConversationItemFolderDisplayer extends FolderDisplayer {
241         private final BidiFormatter mFormatter;
242         private int mFoldersCount;
243 
ConversationItemFolderDisplayer(Context context, BidiFormatter formatter)244         public ConversationItemFolderDisplayer(Context context, BidiFormatter formatter) {
245             super(context);
246             mFormatter = formatter;
247         }
248 
249         @Override
initializeDrawableResources()250         protected void initializeDrawableResources() {
251             super.initializeDrawableResources();
252             final Resources res = mContext.getResources();
253             mFolderDrawableResources.overflowGradientPadding =
254                     res.getDimensionPixelOffset(R.dimen.folder_tl_gradient_padding);
255             mFolderDrawableResources.folderHorizontalPadding =
256                     res.getDimensionPixelOffset(R.dimen.folder_tl_cell_content_padding);
257             mFolderDrawableResources.folderFontSize =
258                     res.getDimensionPixelOffset(R.dimen.folder_tl_font_size);
259         }
260 
261         @Override
loadConversationFolders(Conversation conv, final FolderUri ignoreFolderUri, final int ignoreFolderType)262         public void loadConversationFolders(Conversation conv, final FolderUri ignoreFolderUri,
263                 final int ignoreFolderType) {
264             super.loadConversationFolders(conv, ignoreFolderUri, ignoreFolderType);
265             mFoldersCount = mFoldersSortedSet.size();
266         }
267 
268         @Override
reset()269         public void reset() {
270             super.reset();
271             mFoldersCount = 0;
272         }
273 
hasVisibleFolders()274         public boolean hasVisibleFolders() {
275             return mFoldersCount > 0;
276         }
277 
278         /**
279          * @return how much total space the folders list requires.
280          */
measureFolders(ConversationItemViewCoordinates coordinates)281         private int measureFolders(ConversationItemViewCoordinates coordinates) {
282             final int[] measurements = measureFolderDimen(
283                     mFoldersSortedSet, coordinates.folderCellWidth, coordinates.folderLayoutWidth,
284                     mFolderDrawableResources.folderInBetweenPadding,
285                     mFolderDrawableResources.folderHorizontalPadding, sFoldersMaxCount,
286                     sFoldersPaint);
287             return sumWidth(measurements);
288         }
289 
sumWidth(int[] arr)290         private int sumWidth(int[] arr) {
291             int sum = 0;
292             for (int i : arr) {
293                 sum += i;
294             }
295             return sum + (arr.length - 1) * mFolderDrawableResources.folderInBetweenPadding;
296         }
297 
drawFolders(Canvas canvas, ConversationItemViewCoordinates coordinates, boolean isRtl)298         public void drawFolders(Canvas canvas, ConversationItemViewCoordinates coordinates,
299                 boolean isRtl) {
300             if (mFoldersCount == 0) {
301                 return;
302             }
303 
304             final int[] measurements = measureFolderDimen(
305                     mFoldersSortedSet, coordinates.folderCellWidth, coordinates.folderLayoutWidth,
306                     mFolderDrawableResources.folderInBetweenPadding,
307                     mFolderDrawableResources.folderHorizontalPadding, sFoldersMaxCount,
308                     sFoldersPaint);
309 
310             final int right = coordinates.foldersRight;
311             final int y = coordinates.foldersY;
312 
313             sFoldersPaint.setTextSize(coordinates.foldersFontSize);
314             sFoldersPaint.setTypeface(coordinates.foldersTypeface);
315 
316             // Initialize space and cell size based on the current mode.
317             final Paint.FontMetricsInt fm = sFoldersPaint.getFontMetricsInt();
318             final int foldersCount = measurements.length;
319             final int width = sumWidth(measurements);
320             final int height = fm.bottom - fm.top;
321             int xStart = (isRtl) ? coordinates.snippetX + width : right - width;
322 
323             int index = 0;
324             for (Folder folder : mFoldersSortedSet) {
325                 if (index > foldersCount - 1) {
326                     break;
327                 }
328 
329                 final int actualStart = isRtl ? xStart - measurements[index] : xStart;
330                 drawFolder(canvas, actualStart, y, measurements[index], height, folder,
331                         mFolderDrawableResources, mFormatter, sFoldersPaint);
332 
333                 // Increment the starting position accordingly for the next item
334                 final int usedWidth = measurements[index++] +
335                         mFolderDrawableResources.folderInBetweenPadding;
336                 xStart += (isRtl) ? -usedWidth : usedWidth;
337             }
338         }
339 
getFoldersDesc()340         public @Nullable String getFoldersDesc() {
341             if (mFoldersSortedSet != null && !mFoldersSortedSet.isEmpty()) {
342                 final StringBuilder builder = new StringBuilder();
343                 final String comma = mContext.getString(R.string.enumeration_comma);
344                 for (Folder f : mFoldersSortedSet) {
345                     builder.append(f.name).append(comma);
346                 }
347                 return builder.toString();
348             }
349             return null;
350         }
351     }
352 
ConversationItemView(Context context, Account account)353     public ConversationItemView(Context context, Account account) {
354         super(context);
355         Utils.traceBeginSection("CIVC constructor");
356         setClickable(true);
357         setLongClickable(true);
358         mContext = context.getApplicationContext();
359         final Resources res = mContext.getResources();
360         mTabletDevice = Utils.useTabletUI(res);
361         mListCollapsible = !res.getBoolean(R.bool.is_tablet_landscape);
362         mAccount = account;
363 
364         getItemViewResources(mContext);
365 
366         final int layoutDir = TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault());
367 
368         mSendersTextView = new TextView(mContext);
369         mSendersTextView.setIncludeFontPadding(false);
370 
371         mSubjectTextView = new TextView(mContext);
372         mSubjectTextView.setEllipsize(TextUtils.TruncateAt.END);
373         mSubjectTextView.setIncludeFontPadding(false);
374         ViewCompat.setLayoutDirection(mSubjectTextView, layoutDir);
375         ViewUtils.setTextAlignment(mSubjectTextView, View.TEXT_ALIGNMENT_VIEW_START);
376 
377         mSnippetTextView = new TextView(mContext);
378         mSnippetTextView.setEllipsize(TextUtils.TruncateAt.END);
379         mSnippetTextView.setIncludeFontPadding(false);
380         mSnippetTextView.setTypeface(SANS_SERIF_LIGHT);
381         mSnippetTextView.setTextColor(getResources().getColor(R.color.snippet_text_color));
382         ViewCompat.setLayoutDirection(mSnippetTextView, layoutDir);
383         ViewUtils.setTextAlignment(mSnippetTextView, View.TEXT_ALIGNMENT_VIEW_START);
384 
385         // hack for b/16345519. Root cause is b/17280038.
386         if (layoutDir == LAYOUT_DIRECTION_RTL) {
387             mSubjectTextView.setMaxLines(1);
388             mSnippetTextView.setMaxLines(1);
389         } else {
390             mSubjectTextView.setSingleLine();
391             mSnippetTextView.setSingleLine();
392         }
393 
394         mSendersImageView = new CheckableContactFlipDrawable(res, sCabAnimationDuration);
395         mSendersImageView.setCallback(this);
396 
397         Utils.traceEndSection();
398     }
399 
getItemViewResources(Context context)400     private static synchronized void getItemViewResources(Context context) {
401         if (sConfigurationChangedReceiver == null) {
402             sConfigurationChangedReceiver = new BroadcastReceiver() {
403                 @Override
404                 public void onReceive(Context context, Intent intent) {
405                     STAR_OFF = null;
406                     getItemViewResources(context);
407                 }
408             };
409             context.registerReceiver(sConfigurationChangedReceiver, new IntentFilter(
410                     Intent.ACTION_CONFIGURATION_CHANGED));
411         }
412         if (STAR_OFF == null) {
413             final Resources res = context.getResources();
414             // Initialize static bitmaps.
415             STAR_OFF = BitmapFactory.decodeResource(res, R.drawable.ic_star_outline_20dp);
416             STAR_ON = BitmapFactory.decodeResource(res, R.drawable.ic_star_20dp);
417             ATTACHMENT = BitmapFactory.decodeResource(res, R.drawable.ic_attach_file_18dp);
418             ONLY_TO_ME = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_double);
419             TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_single);
420             IMPORTANT_ONLY_TO_ME = BitmapFactory.decodeResource(res,
421                     R.drawable.ic_email_caret_double_important_unread);
422             IMPORTANT_TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res,
423                     R.drawable.ic_email_caret_single_important_unread);
424             IMPORTANT = BitmapFactory.decodeResource(res,
425                     R.drawable.ic_email_caret_none_important_unread);
426             STATE_REPLIED =
427                     BitmapFactory.decodeResource(res, R.drawable.ic_badge_reply_holo_light);
428             STATE_FORWARDED =
429                     BitmapFactory.decodeResource(res, R.drawable.ic_badge_forward_holo_light);
430             STATE_REPLIED_AND_FORWARDED =
431                     BitmapFactory.decodeResource(res, R.drawable.ic_badge_reply_forward_holo_light);
432             STATE_CALENDAR_INVITE =
433                     BitmapFactory.decodeResource(res, R.drawable.ic_badge_invite_holo_light);
434             FOCUSED_CONVERSATION_HIGHLIGHT = res.getDrawable(
435                     R.drawable.visible_conversation_highlight);
436 
437             // Initialize colors.
438             sActivatedTextSpan = CharacterStyle.wrap(new ForegroundColorSpan(
439                     res.getColor(R.color.senders_text_color)));
440             sSendersTextColor = res.getColor(R.color.senders_text_color);
441             sSubjectTextUnreadSpan = new TextAppearanceSpan(context,
442                     R.style.SubjectAppearanceUnreadStyle);
443             sSubjectTextReadSpan = new TextAppearanceSpan(
444                     context, R.style.SubjectAppearanceReadStyle);
445 
446             sBadgeTextSpan = new TextAppearanceSpan(context, R.style.BadgeTextStyle);
447             sBadgeBackgroundSpan = new BackgroundColorSpan(
448                     res.getColor(R.color.badge_background_color));
449             sDateTextColorRead = res.getColor(R.color.date_text_color_read);
450             sDateTextColorUnread = res.getColor(R.color.date_text_color_unread);
451             sStarTouchSlop = res.getDimensionPixelSize(R.dimen.star_touch_slop);
452             sSenderImageTouchSlop = res.getDimensionPixelSize(R.dimen.sender_image_touch_slop);
453             sShrinkAnimationDuration = res.getInteger(R.integer.shrink_animation_duration);
454             sSlideAnimationDuration = res.getInteger(R.integer.slide_animation_duration);
455             // Initialize static color.
456             sSendersSplitToken = res.getString(R.string.senders_split_token);
457             sElidedPaddingToken = res.getString(R.string.elided_padding_token);
458             sScrollSlop = res.getInteger(R.integer.swipeScrollSlop);
459             sFoldersMaxCount = res.getInteger(R.integer.conversation_list_max_folder_count);
460             sCabAnimationDuration = res.getInteger(R.integer.conv_item_view_cab_anim_duration);
461             sBadgePaddingExtraWidth = res.getDimensionPixelSize(R.dimen.badge_padding_extra_width);
462             sBadgeRoundedCornerRadius =
463                     res.getDimensionPixelSize(R.dimen.badge_rounded_corner_radius);
464             sDividerPaint.setColor(res.getColor(R.color.divider_color));
465             sDividerHeight = res.getDimensionPixelSize(R.dimen.divider_height);
466         }
467     }
468 
bind(final Conversation conversation, final ControllableActivity activity, final ConversationCheckedSet set, final Folder folder, final int checkboxOrSenderImage, final boolean swipeEnabled, final boolean importanceMarkersEnabled, final boolean showChevronsEnabled, final AnimatedAdapter adapter)469     public void bind(final Conversation conversation, final ControllableActivity activity,
470             final ConversationCheckedSet set, final Folder folder,
471             final int checkboxOrSenderImage,
472             final boolean swipeEnabled, final boolean importanceMarkersEnabled,
473             final boolean showChevronsEnabled, final AnimatedAdapter adapter) {
474         Utils.traceBeginSection("CIVC.bind");
475         bind(ConversationItemViewModel.forConversation(mAccount.getEmailAddress(), conversation),
476                 activity, null /* conversationItemAreaClickListener */,
477                 set, folder, checkboxOrSenderImage, swipeEnabled, importanceMarkersEnabled,
478                 showChevronsEnabled, adapter, -1 /* backgroundOverrideResId */,
479                 null /* photoBitmap */, false /* useFullMargins */, true /* mDividerEnabled */);
480         Utils.traceEndSection();
481     }
482 
bindAd(final ConversationItemViewModel conversationItemViewModel, final ControllableActivity activity, final ConversationItemAreaClickListener conversationItemAreaClickListener, final Folder folder, final int checkboxOrSenderImage, final AnimatedAdapter adapter, final int backgroundOverrideResId, final Bitmap photoBitmap)483     public void bindAd(final ConversationItemViewModel conversationItemViewModel,
484             final ControllableActivity activity,
485             final ConversationItemAreaClickListener conversationItemAreaClickListener,
486             final Folder folder, final int checkboxOrSenderImage, final AnimatedAdapter adapter,
487             final int backgroundOverrideResId, final Bitmap photoBitmap) {
488         Utils.traceBeginSection("CIVC.bindAd");
489         bind(conversationItemViewModel, activity, conversationItemAreaClickListener, null /* set */,
490                 folder, checkboxOrSenderImage, true /* swipeEnabled */,
491                 false /* importanceMarkersEnabled */, false /* showChevronsEnabled */,
492                 adapter, backgroundOverrideResId, photoBitmap, true /* useFullMargins */,
493                 false /* mDividerEnabled */);
494         Utils.traceEndSection();
495     }
496 
bind(final ConversationItemViewModel header, final ControllableActivity activity, final ConversationItemAreaClickListener conversationItemAreaClickListener, final ConversationCheckedSet set, final Folder folder, final int checkboxOrSenderImage, boolean swipeEnabled, final boolean importanceMarkersEnabled, final boolean showChevronsEnabled, final AnimatedAdapter adapter, final int backgroundOverrideResId, final Bitmap photoBitmap, final boolean useFullMargins, final boolean dividerEnabled)497     private void bind(final ConversationItemViewModel header, final ControllableActivity activity,
498             final ConversationItemAreaClickListener conversationItemAreaClickListener,
499             final ConversationCheckedSet set, final Folder folder,
500             final int checkboxOrSenderImage,
501             boolean swipeEnabled, final boolean importanceMarkersEnabled,
502             final boolean showChevronsEnabled, final AnimatedAdapter adapter,
503             final int backgroundOverrideResId, final Bitmap photoBitmap,
504             final boolean useFullMargins, final boolean dividerEnabled) {
505         mBackgroundOverrideResId = backgroundOverrideResId;
506         mPhotoBitmap = photoBitmap;
507         mConversationItemAreaClickListener = conversationItemAreaClickListener;
508         mDividerEnabled = dividerEnabled;
509 
510         if (mHeader != null) {
511             Utils.traceBeginSection("unbind");
512             final boolean newlyBound = header.conversation.id != mHeader.conversation.id;
513             // If this was previously bound to a different conversation, remove any contact photo
514             // manager requests.
515             if (newlyBound || (!mHeader.displayableNames.equals(header.displayableNames))) {
516                 mSendersImageView.getContactDrawable().unbind();
517             }
518 
519             if (newlyBound) {
520                 // Stop the photo flip animation
521                 final boolean showSenders = !mChecked;
522                 mSendersImageView.reset(showSenders);
523             }
524             Utils.traceEndSection();
525         }
526         mCoordinates = null;
527         mHeader = header;
528         mActivity = activity;
529         mCheckedConversationSet = set;
530         if (mCheckedConversationSet != null) {
531             mCheckedConversationSet.addObserver(this);
532         }
533         mDisplayedFolder = folder;
534         mStarEnabled = folder != null && !folder.isTrash();
535         mSwipeEnabled = swipeEnabled;
536         mAdapter = adapter;
537 
538         Utils.traceBeginSection("drawables");
539         mSendersImageView.getContactDrawable().setBitmapCache(mAdapter.getSendersImagesCache());
540         mSendersImageView.getContactDrawable().setContactResolver(mAdapter.getContactResolver());
541         Utils.traceEndSection();
542 
543         if (checkboxOrSenderImage == ConversationListIcon.SENDER_IMAGE) {
544             mGadgetMode = ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO;
545         } else {
546             mGadgetMode = ConversationItemViewCoordinates.GADGET_NONE;
547         }
548 
549         Utils.traceBeginSection("folder displayer");
550         // Initialize folder displayer.
551         if (mHeader.folderDisplayer == null) {
552             mHeader.folderDisplayer = new ConversationItemFolderDisplayer(mContext,
553                     mAdapter.getBidiFormatter());
554         } else {
555             mHeader.folderDisplayer.reset();
556         }
557         Utils.traceEndSection();
558 
559         final int ignoreFolderType;
560         if (mDisplayedFolder.isInbox()) {
561             ignoreFolderType = FolderType.INBOX;
562         } else {
563             ignoreFolderType = -1;
564         }
565 
566         Utils.traceBeginSection("load folders");
567         mHeader.folderDisplayer.loadConversationFolders(mHeader.conversation,
568                 mDisplayedFolder.folderUri, ignoreFolderType);
569         Utils.traceEndSection();
570 
571         if (mHeader.showDateText) {
572             Utils.traceBeginSection("relative time");
573             mHeader.dateText = DateUtils.getRelativeTimeSpanString(mContext,
574                     mHeader.conversation.dateMs);
575             Utils.traceEndSection();
576         } else {
577             mHeader.dateText = "";
578         }
579 
580         Utils.traceBeginSection("config setup");
581         mConfig = new ConversationItemViewCoordinates.Config()
582             .withGadget(mGadgetMode)
583             .setUseFullMargins(useFullMargins);
584         if (header.folderDisplayer.hasVisibleFolders()) {
585             mConfig.showFolders();
586         }
587         if (header.hasBeenForwarded || header.hasBeenRepliedTo || header.isInvite) {
588             mConfig.showReplyState();
589         }
590         if (mHeader.conversation.color != 0) {
591             mConfig.showColorBlock();
592         }
593 
594         // Importance markers and chevrons (personal level indicators).
595         mHeader.personalLevelBitmap = null;
596         final int personalLevel = mHeader.conversation.personalLevel;
597         final boolean isImportant =
598                 mHeader.conversation.priority == UIProvider.ConversationPriority.IMPORTANT;
599         final boolean useImportantMarkers = isImportant && importanceMarkersEnabled;
600         if (showChevronsEnabled &&
601                 personalLevel == UIProvider.ConversationPersonalLevel.ONLY_TO_ME) {
602             mHeader.personalLevelBitmap = useImportantMarkers ? IMPORTANT_ONLY_TO_ME
603                     : ONLY_TO_ME;
604         } else if (showChevronsEnabled &&
605                 personalLevel == UIProvider.ConversationPersonalLevel.TO_ME_AND_OTHERS) {
606             mHeader.personalLevelBitmap = useImportantMarkers ? IMPORTANT_TO_ME_AND_OTHERS
607                     : TO_ME_AND_OTHERS;
608         } else if (useImportantMarkers) {
609             mHeader.personalLevelBitmap = IMPORTANT;
610         }
611         if (mHeader.personalLevelBitmap != null) {
612             mConfig.showPersonalIndicator();
613         }
614         Utils.traceEndSection();
615 
616         Utils.traceBeginSection("content description");
617         setContentDescription();
618         Utils.traceEndSection();
619         requestLayout();
620     }
621 
622     @Override
onDetachedFromWindow()623     protected void onDetachedFromWindow() {
624         super.onDetachedFromWindow();
625 
626         if (mCheckedConversationSet != null) {
627             mCheckedConversationSet.removeObserver(this);
628         }
629     }
630 
631     @Override
invalidateDrawable(final Drawable who)632     public void invalidateDrawable(final Drawable who) {
633         boolean handled = false;
634         if (mCoordinates != null) {
635             if (mSendersImageView.equals(who)) {
636                 final Rect r = new Rect(who.getBounds());
637                 r.offset(mCoordinates.contactImagesX, mCoordinates.contactImagesY);
638                 ConversationItemView.this.invalidate(r.left, r.top, r.right, r.bottom);
639                 handled = true;
640             }
641         }
642         if (!handled) {
643             super.invalidateDrawable(who);
644         }
645     }
646 
647     /**
648      * Get the Conversation object associated with this view.
649      */
getConversation()650     public Conversation getConversation() {
651         return mHeader.conversation;
652     }
653 
startTimer(String tag)654     private static void startTimer(String tag) {
655         if (sTimer != null) {
656             sTimer.start(tag);
657         }
658     }
659 
pauseTimer(String tag)660     private static void pauseTimer(String tag) {
661         if (sTimer != null) {
662             sTimer.pause(tag);
663         }
664     }
665 
666     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)667     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
668         Utils.traceBeginSection("CIVC.measure");
669         final int wSize = MeasureSpec.getSize(widthMeasureSpec);
670 
671         final int currentMode = mActivity.getViewMode().getMode();
672         if (wSize != mViewWidth || mPreviousMode != currentMode) {
673             mViewWidth = wSize;
674             mPreviousMode = currentMode;
675         }
676         mHeader.viewWidth = mViewWidth;
677 
678         mConfig.updateWidth(wSize).setLayoutDirection(ViewCompat.getLayoutDirection(this));
679 
680         Resources res = getResources();
681         mHeader.standardScaledDimen = res.getDimensionPixelOffset(R.dimen.standard_scaled_dimen);
682 
683         mCoordinates = ConversationItemViewCoordinates.forConfig(mContext, mConfig,
684                 mAdapter.getCoordinatesCache());
685 
686         if (mPhotoBitmap != null) {
687             mPhotoRect.set(0, 0, mCoordinates.contactImagesWidth, mCoordinates.contactImagesHeight);
688         }
689 
690         final int h = (mAnimatedHeightFraction != 1.0f) ?
691                 Math.round(mAnimatedHeightFraction * mCoordinates.height) : mCoordinates.height;
692         setMeasuredDimension(mConfig.getWidth(), h);
693         Utils.traceEndSection();
694     }
695 
696     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)697     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
698         startTimer(PERF_TAG_LAYOUT);
699         Utils.traceBeginSection("CIVC.layout");
700 
701         super.onLayout(changed, left, top, right, bottom);
702 
703         Utils.traceBeginSection("text and bitmaps");
704         calculateTextsAndBitmaps();
705         Utils.traceEndSection();
706 
707         Utils.traceBeginSection("coordinates");
708         calculateCoordinates();
709         Utils.traceEndSection();
710 
711         // Subject.
712         Utils.traceBeginSection("subject");
713         createSubject(mHeader.unread);
714 
715         createSnippet();
716 
717         if (!mHeader.isLayoutValid()) {
718             setContentDescription();
719         }
720         mHeader.validate();
721         Utils.traceEndSection();
722 
723         pauseTimer(PERF_TAG_LAYOUT);
724         if (sTimer != null && ++sLayoutCount >= PERF_LAYOUT_ITERATIONS) {
725             sTimer.dumpResults();
726             sTimer = new Timer();
727             sLayoutCount = 0;
728         }
729         Utils.traceEndSection();
730     }
731 
setContentDescription()732     private void setContentDescription() {
733         String foldersDesc = null;
734         if (mHeader != null && mHeader.folderDisplayer != null) {
735             foldersDesc = mHeader.folderDisplayer.getFoldersDesc();
736         }
737 
738         if (mActivity.isAccessibilityEnabled()) {
739             mHeader.resetContentDescription();
740             setContentDescription(mHeader.getContentDescription(
741                     mContext, mDisplayedFolder.shouldShowRecipients(), foldersDesc));
742         }
743     }
744 
745     @Override
setBackgroundResource(int resourceId)746     public void setBackgroundResource(int resourceId) {
747         Utils.traceBeginSection("set background resource");
748         Drawable drawable = mBackgrounds.get(resourceId);
749         if (drawable == null) {
750             drawable = getResources().getDrawable(resourceId);
751             final int insetPadding = mHeader.insetPadding;
752             if (insetPadding > 0) {
753                 drawable = new InsetDrawable(drawable, insetPadding);
754             }
755             mBackgrounds.put(resourceId, drawable);
756         }
757         if (getBackground() != drawable) {
758             super.setBackgroundDrawable(drawable);
759         }
760         Utils.traceEndSection();
761     }
762 
calculateTextsAndBitmaps()763     private void calculateTextsAndBitmaps() {
764         startTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
765 
766         if (mCheckedConversationSet != null) {
767             setChecked(mCheckedConversationSet.contains(mHeader.conversation));
768         }
769         mHeader.gadgetMode = mGadgetMode;
770 
771         updateBackground();
772 
773         mHeader.hasDraftMessage = mHeader.conversation.numDrafts() > 0;
774 
775         // Parse senders fragments.
776         if (mHeader.preserveSendersText) {
777             // This is a special view that doesn't need special sender formatting
778             mHeader.sendersDisplayText = new SpannableStringBuilder(mHeader.sendersText);
779             loadImages();
780         } else if (mHeader.conversation.conversationInfo != null) {
781             Context context = getContext();
782             mHeader.messageInfoString = SendersView
783                     .createMessageInfo(context, mHeader.conversation, true);
784             final int maxChars = ConversationItemViewCoordinates.getSendersLength(context,
785                     mHeader.conversation.hasAttachments);
786 
787             mHeader.mSenderAvatarModel.clear();
788             mHeader.displayableNames.clear();
789             mHeader.styledNames.clear();
790 
791             SendersView.format(context, mHeader.conversation.conversationInfo,
792                     mHeader.messageInfoString.toString(), maxChars, mHeader.styledNames,
793                     mHeader.displayableNames, mHeader.mSenderAvatarModel,
794                     mAccount, mDisplayedFolder.shouldShowRecipients(), true);
795 
796             // If we have displayable senders, load their thumbnails
797             loadImages();
798         } else {
799             LogUtils.wtf(LOG_TAG, "Null conversationInfo");
800         }
801 
802         if (mHeader.isLayoutValid()) {
803             pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
804             return;
805         }
806         startTimer(PERF_TAG_CALCULATE_FOLDERS);
807 
808 
809         pauseTimer(PERF_TAG_CALCULATE_FOLDERS);
810 
811         // Paper clip icon.
812         mHeader.paperclip = null;
813         if (mHeader.conversation.hasAttachments) {
814             mHeader.paperclip = ATTACHMENT;
815         }
816 
817         startTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT);
818 
819         pauseTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT);
820         pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
821     }
822 
823     // FIXME(ath): maybe move this to bind(). the only dependency on layout is on tile W/H, which
824     // is immutable.
loadImages()825     private void loadImages() {
826         if (mGadgetMode != ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO
827                 || mHeader.mSenderAvatarModel.isNotPopulated()) {
828             return;
829         }
830         if (mCoordinates.contactImagesWidth <= 0 || mCoordinates.contactImagesHeight <= 0) {
831             LogUtils.w(LOG_TAG,
832                     "Contact image width(%d) or height(%d) is 0",
833                     mCoordinates.contactImagesWidth, mCoordinates.contactImagesHeight);
834             return;
835         }
836 
837         mSendersImageView
838                 .setBounds(0, 0, mCoordinates.contactImagesWidth, mCoordinates.contactImagesHeight);
839 
840         Utils.traceBeginSection("load sender image");
841         final ContactDrawable drawable = mSendersImageView.getContactDrawable();
842         drawable.setDecodeDimensions(mCoordinates.contactImagesWidth,
843                 mCoordinates.contactImagesHeight);
844         drawable.bind(mHeader.mSenderAvatarModel.getName(),
845                 mHeader.mSenderAvatarModel.getEmailAddress());
846         Utils.traceEndSection();
847     }
848 
makeExactSpecForSize(int size)849     private static int makeExactSpecForSize(int size) {
850         return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
851     }
852 
layoutViewExactly(View v, int w, int h)853     private static void layoutViewExactly(View v, int w, int h) {
854         v.measure(makeExactSpecForSize(w), makeExactSpecForSize(h));
855         v.layout(0, 0, w, h);
856     }
857 
layoutParticipantText(SpannableStringBuilder participantText)858     private void layoutParticipantText(SpannableStringBuilder participantText) {
859         if (participantText != null) {
860             if (isActivated() && showActivatedText()) {
861                 participantText.setSpan(sActivatedTextSpan, 0,
862                         mHeader.styledMessageInfoStringOffset, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
863             } else {
864                 participantText.removeSpan(sActivatedTextSpan);
865             }
866 
867             final int w = mSendersWidth;
868             final int h = mCoordinates.sendersHeight;
869             mSendersTextView.setLayoutParams(new ViewGroup.LayoutParams(w, h));
870             mSendersTextView.setMaxLines(mCoordinates.sendersLineCount);
871             mSendersTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCoordinates.sendersFontSize);
872             layoutViewExactly(mSendersTextView, w, h);
873 
874             mSendersTextView.setText(participantText);
875         }
876     }
877 
createSubject(final boolean isUnread)878     private void createSubject(final boolean isUnread) {
879         final String badgeText = mHeader.badgeText == null ? "" : mHeader.badgeText;
880         String subject = filterTag(getContext(), mHeader.conversation.subject);
881         subject = mAdapter.getBidiFormatter().unicodeWrap(subject);
882         subject = Conversation.getSubjectForDisplay(mContext, badgeText, subject);
883         final Spannable displayedStringBuilder = new SpannableString(subject);
884 
885         // since spans affect text metrics, add spans to the string before measure/layout or eliding
886 
887         final int badgeTextLength = formatBadgeText(displayedStringBuilder, badgeText);
888 
889         if (!TextUtils.isEmpty(subject)) {
890             displayedStringBuilder.setSpan(TextAppearanceSpan.wrap(
891                     isUnread ? sSubjectTextUnreadSpan : sSubjectTextReadSpan),
892                     badgeTextLength, subject.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
893         }
894         if (isActivated() && showActivatedText()) {
895             displayedStringBuilder.setSpan(sActivatedTextSpan, badgeTextLength,
896                     displayedStringBuilder.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
897         }
898 
899         final int subjectWidth = mCoordinates.subjectWidth;
900         final int subjectHeight = mCoordinates.subjectHeight;
901         mSubjectTextView.setLayoutParams(new ViewGroup.LayoutParams(subjectWidth, subjectHeight));
902         mSubjectTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCoordinates.subjectFontSize);
903         layoutViewExactly(mSubjectTextView, subjectWidth, subjectHeight);
904 
905         mSubjectTextView.setText(displayedStringBuilder);
906     }
907 
createSnippet()908     private void createSnippet() {
909         final String snippet = mHeader.conversation.getSnippet();
910         final Spannable displayedStringBuilder = new SpannableString(snippet);
911 
912         // measure the width of the folders which overlap the snippet view
913         final int folderWidth = mHeader.folderDisplayer.measureFolders(mCoordinates);
914 
915         // size the snippet view by subtracting the folder width from the maximum snippet width
916         final int snippetWidth = mCoordinates.maxSnippetWidth - folderWidth;
917         final int snippetHeight = mCoordinates.snippetHeight;
918         mSnippetTextView.setLayoutParams(new ViewGroup.LayoutParams(snippetWidth, snippetHeight));
919         mSnippetTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCoordinates.snippetFontSize);
920         layoutViewExactly(mSnippetTextView, snippetWidth, snippetHeight);
921 
922         mSnippetTextView.setText(displayedStringBuilder);
923     }
924 
formatBadgeText(Spannable displayedStringBuilder, String badgeText)925     private int formatBadgeText(Spannable displayedStringBuilder, String badgeText) {
926         final int badgeTextLength = (badgeText != null) ? badgeText.length() : 0;
927         if (!TextUtils.isEmpty(badgeText)) {
928             displayedStringBuilder.setSpan(TextAppearanceSpan.wrap(sBadgeTextSpan),
929                     0, badgeTextLength, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
930             displayedStringBuilder.setSpan(TextAppearanceSpan.wrap(sBadgeBackgroundSpan),
931                     0, badgeTextLength, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
932             displayedStringBuilder.setSpan(new BadgeSpan(displayedStringBuilder, this),
933                     0, badgeTextLength, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
934         }
935 
936         return badgeTextLength;
937     }
938 
939     // START BadgeSpan.BadgeSpanDimensions override
940 
941     @Override
getHorizontalPadding()942     public int getHorizontalPadding() {
943         return sBadgePaddingExtraWidth;
944     }
945 
946     @Override
getRoundedCornerRadius()947     public float getRoundedCornerRadius() {
948         return sBadgeRoundedCornerRadius;
949     }
950 
951     // END BadgeSpan.BadgeSpanDimensions override
952 
showActivatedText()953     private boolean showActivatedText() {
954         // For activated elements in tablet in conversation mode, we show an activated color, since
955         // the background is dark blue for activated versus gray for non-activated.
956         return mTabletDevice && !mListCollapsible;
957     }
958 
calculateCoordinates()959     private void calculateCoordinates() {
960         startTimer(PERF_TAG_CALCULATE_COORDINATES);
961 
962         sPaint.setTextSize(mCoordinates.dateFontSize);
963         sPaint.setTypeface(Typeface.DEFAULT);
964 
965         final boolean isRtl = ViewUtils.isViewRtl(this);
966 
967         mDateWidth = (int) sPaint.measureText(
968                 mHeader.dateText != null ? mHeader.dateText.toString() : "");
969         if (mHeader.infoIcon != null) {
970             mInfoIconX = (isRtl) ? mCoordinates.infoIconX :
971                     mCoordinates.infoIconXRight - mHeader.infoIcon.getWidth();
972 
973             // If we have an info icon, we start drawing the date text:
974             // At the end of the date TextView minus the width of the date text
975             // In RTL mode, we just use dateX
976             mDateX = (isRtl) ? mCoordinates.dateX : mCoordinates.dateXRight - mDateWidth;
977         } else {
978             // If there is no info icon, we start drawing the date text:
979             // At the end of the info icon ImageView minus the width of the date text
980             // We use the info icon ImageView for positioning, since we want the date text to be
981             // at the right, since there is no info icon
982             // In RTL, we just use infoIconX
983             mDateX = (isRtl) ? mCoordinates.infoIconX : mCoordinates.infoIconXRight - mDateWidth;
984         }
985 
986         // The paperclip is drawn starting at the start of the date text minus
987         // the width of the paperclip and the date padding.
988         // In RTL mode, it is at the end of the date (mDateX + mDateWidth) plus the
989         // start date padding.
990         mPaperclipX = (isRtl) ? mDateX + mDateWidth + mCoordinates.datePaddingStart :
991                 mDateX - ATTACHMENT.getWidth() - mCoordinates.datePaddingStart;
992 
993         // In normal mode, the senders x and width is based
994         // on where the date/attachment icon start.
995         final int dateAttachmentStart;
996         // Have this end near the paperclip or date, not the folders.
997         if (mHeader.paperclip != null) {
998             // If there is a paperclip, the date/attachment start is at the start
999             // of the paperclip minus the paperclip padding.
1000             // In RTL, it is at the end of the paperclip plus the paperclip padding.
1001             dateAttachmentStart = (isRtl) ?
1002                     mPaperclipX + ATTACHMENT.getWidth() + mCoordinates.paperclipPaddingStart
1003                     : mPaperclipX - mCoordinates.paperclipPaddingStart;
1004         } else {
1005             // If no paperclip, just use the start of the date minus the date padding start.
1006             // In RTL mode, this is just the paperclipX.
1007             dateAttachmentStart = (isRtl) ?
1008                     mPaperclipX : mDateX - mCoordinates.datePaddingStart;
1009         }
1010         // Senders width is the dateAttachmentStart - sendersX.
1011         // In RTL, it is sendersWidth + sendersX - dateAttachmentStart.
1012         mSendersWidth = (isRtl) ?
1013                 mCoordinates.sendersWidth + mCoordinates.sendersX - dateAttachmentStart
1014                 : dateAttachmentStart - mCoordinates.sendersX;
1015         mSendersX = (isRtl) ? dateAttachmentStart : mCoordinates.sendersX;
1016 
1017         // Second pass to layout each fragment.
1018         sPaint.setTextSize(mCoordinates.sendersFontSize);
1019         sPaint.setTypeface(Typeface.DEFAULT);
1020 
1021         // First pass to calculate width of each fragment.
1022         if (mSendersWidth < 0) {
1023             mSendersWidth = 0;
1024         }
1025 
1026         // sendersDisplayText is only set when preserveSendersText is true.
1027         if (mHeader.preserveSendersText) {
1028             mHeader.sendersDisplayLayout = new StaticLayout(mHeader.sendersDisplayText, sPaint,
1029                     mSendersWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
1030         } else {
1031             final SpannableStringBuilder participantText = elideParticipants(mHeader.styledNames);
1032             layoutParticipantText(participantText);
1033         }
1034 
1035         pauseTimer(PERF_TAG_CALCULATE_COORDINATES);
1036     }
1037 
1038     // The rules for displaying elided participants are as follows:
1039     // 1) If there is message info (either a COUNT or DRAFT info to display), it MUST be shown
1040     // 2) If senders do not fit, ellipsize the last one that does fit, and stop
1041     // appending new senders
elideParticipants(List<SpannableString> parts)1042     SpannableStringBuilder elideParticipants(List<SpannableString> parts) {
1043         final SpannableStringBuilder builder = new SpannableStringBuilder();
1044         float totalWidth = 0;
1045         boolean ellipsize = false;
1046         float width;
1047         boolean skipToHeader = false;
1048 
1049         // start with "To: " if we're showing recipients
1050         if (mDisplayedFolder.shouldShowRecipients() && !parts.isEmpty()) {
1051             final SpannableString toHeader = SendersView.getFormattedToHeader();
1052             CharacterStyle[] spans = toHeader.getSpans(0, toHeader.length(),
1053                     CharacterStyle.class);
1054             // There is only 1 character style span; make sure we apply all the
1055             // styles to the paint object before measuring.
1056             if (spans.length > 0) {
1057                 spans[0].updateDrawState(sPaint);
1058             }
1059             totalWidth += sPaint.measureText(toHeader.toString());
1060             builder.append(toHeader);
1061             skipToHeader = true;
1062         }
1063 
1064         final SpannableStringBuilder messageInfoString = mHeader.messageInfoString;
1065         if (!TextUtils.isEmpty(messageInfoString)) {
1066             CharacterStyle[] spans = messageInfoString.getSpans(0, messageInfoString.length(),
1067                     CharacterStyle.class);
1068             // There is only 1 character style span; make sure we apply all the
1069             // styles to the paint object before measuring.
1070             if (spans.length > 0) {
1071                 spans[0].updateDrawState(sPaint);
1072             }
1073             // Paint the message info string to see if we lose space.
1074             float messageInfoWidth = sPaint.measureText(messageInfoString.toString());
1075             totalWidth += messageInfoWidth;
1076         }
1077         SpannableString prevSender = null;
1078         SpannableString ellipsizedText;
1079         for (SpannableString sender : parts) {
1080             // There may be null sender strings if there were dupes we had to remove.
1081             if (sender == null) {
1082                 continue;
1083             }
1084             // No more width available, we'll only show fixed fragments.
1085             if (ellipsize) {
1086                 break;
1087             }
1088             CharacterStyle[] spans = sender.getSpans(0, sender.length(), CharacterStyle.class);
1089             // There is only 1 character style span.
1090             if (spans.length > 0) {
1091                 spans[0].updateDrawState(sPaint);
1092             }
1093             // If there are already senders present in this string, we need to
1094             // make sure we prepend the dividing token
1095             if (SendersView.sElidedString.equals(sender.toString())) {
1096                 sender = copyStyles(spans, sElidedPaddingToken + sender + sElidedPaddingToken);
1097             } else if (!skipToHeader && builder.length() > 0
1098                     && (prevSender == null || !SendersView.sElidedString.equals(prevSender
1099                             .toString()))) {
1100                 sender = copyStyles(spans, sSendersSplitToken + sender);
1101             } else {
1102                 skipToHeader = false;
1103             }
1104             prevSender = sender;
1105 
1106             if (spans.length > 0) {
1107                 spans[0].updateDrawState(sPaint);
1108             }
1109             // Measure the width of the current sender and make sure we have space
1110             width = (int) sPaint.measureText(sender.toString());
1111             if (width + totalWidth > mSendersWidth) {
1112                 // The text is too long, new line won't help. We have to
1113                 // ellipsize text.
1114                 ellipsize = true;
1115                 width = mSendersWidth - totalWidth; // ellipsis width?
1116                 ellipsizedText = copyStyles(spans,
1117                         TextUtils.ellipsize(sender, sPaint, width, TruncateAt.END));
1118                 width = (int) sPaint.measureText(ellipsizedText.toString());
1119             } else {
1120                 ellipsizedText = null;
1121             }
1122             totalWidth += width;
1123 
1124             final CharSequence fragmentDisplayText;
1125             if (ellipsizedText != null) {
1126                 fragmentDisplayText = ellipsizedText;
1127             } else {
1128                 fragmentDisplayText = sender;
1129             }
1130             builder.append(fragmentDisplayText);
1131         }
1132         mHeader.styledMessageInfoStringOffset = builder.length();
1133         if (!TextUtils.isEmpty(messageInfoString)) {
1134             builder.append(messageInfoString);
1135         }
1136         return builder;
1137     }
1138 
copyStyles(CharacterStyle[] spans, CharSequence newText)1139     private static SpannableString copyStyles(CharacterStyle[] spans, CharSequence newText) {
1140         SpannableString s = new SpannableString(newText);
1141         if (spans != null && spans.length > 0) {
1142             s.setSpan(spans[0], 0, s.length(), 0);
1143         }
1144         return s;
1145     }
1146 
1147     /**
1148      * If the subject contains the tag of a mailing-list (text surrounded with
1149      * []), return the subject with that tag ellipsized, e.g.
1150      * "[android-gmail-team] Hello" -> "[andr...] Hello"
1151      */
filterTag(Context context, String subject)1152     public static String filterTag(Context context, String subject) {
1153         String result = subject;
1154         String formatString = context.getResources().getString(R.string.filtered_tag);
1155         if (!TextUtils.isEmpty(subject) && subject.charAt(0) == '[') {
1156             int end = subject.indexOf(']');
1157             if (end > 0) {
1158                 String tag = subject.substring(1, end);
1159                 result = String.format(formatString, Utils.ellipsize(tag, 7),
1160                         subject.substring(end + 1));
1161             }
1162         }
1163         return result;
1164     }
1165 
1166     @Override
onDraw(Canvas canvas)1167     protected void onDraw(Canvas canvas) {
1168         if (mCoordinates == null) {
1169             LogUtils.e(LOG_TAG, "null coordinates in ConversationItemView#onDraw");
1170             return;
1171         }
1172 
1173         Utils.traceBeginSection("CIVC.draw");
1174 
1175         // Contact photo
1176         if (mGadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO) {
1177             canvas.save();
1178             Utils.traceBeginSection("draw senders image");
1179             drawSendersImage(canvas);
1180             Utils.traceEndSection();
1181             canvas.restore();
1182         }
1183 
1184         // Senders.
1185         boolean isUnread = mHeader.unread;
1186         // Old style senders; apply text colors/ sizes/ styling.
1187         canvas.save();
1188         if (mHeader.sendersDisplayLayout != null) {
1189             sPaint.setTextSize(mCoordinates.sendersFontSize);
1190             sPaint.setTypeface(SendersView.getTypeface(isUnread));
1191             sPaint.setColor(sSendersTextColor);
1192             canvas.translate(mSendersX, mCoordinates.sendersY
1193                     + mHeader.sendersDisplayLayout.getTopPadding());
1194             mHeader.sendersDisplayLayout.draw(canvas);
1195         } else {
1196             drawSenders(canvas);
1197         }
1198         canvas.restore();
1199 
1200 
1201         // Subject.
1202         sPaint.setTypeface(Typeface.DEFAULT);
1203         canvas.save();
1204         drawSubject(canvas);
1205         canvas.restore();
1206 
1207         canvas.save();
1208         drawSnippet(canvas);
1209         canvas.restore();
1210 
1211         // Folders.
1212         if (mConfig.areFoldersVisible()) {
1213             mHeader.folderDisplayer.drawFolders(canvas, mCoordinates, ViewUtils.isViewRtl(this));
1214         }
1215 
1216         // If this folder has a color (combined view/Email), show it here
1217         if (mConfig.isColorBlockVisible()) {
1218             sFoldersPaint.setColor(mHeader.conversation.color);
1219             sFoldersPaint.setStyle(Paint.Style.FILL);
1220             canvas.drawRect(mCoordinates.colorBlockX, mCoordinates.colorBlockY,
1221                     mCoordinates.colorBlockX + mCoordinates.colorBlockWidth,
1222                     mCoordinates.colorBlockY + mCoordinates.colorBlockHeight, sFoldersPaint);
1223         }
1224 
1225         // Draw the reply state. Draw nothing if neither replied nor forwarded.
1226         if (mConfig.isReplyStateVisible()) {
1227             if (mHeader.hasBeenRepliedTo && mHeader.hasBeenForwarded) {
1228                 canvas.drawBitmap(STATE_REPLIED_AND_FORWARDED, mCoordinates.replyStateX,
1229                         mCoordinates.replyStateY, null);
1230             } else if (mHeader.hasBeenRepliedTo) {
1231                 canvas.drawBitmap(STATE_REPLIED, mCoordinates.replyStateX,
1232                         mCoordinates.replyStateY, null);
1233             } else if (mHeader.hasBeenForwarded) {
1234                 canvas.drawBitmap(STATE_FORWARDED, mCoordinates.replyStateX,
1235                         mCoordinates.replyStateY, null);
1236             } else if (mHeader.isInvite) {
1237                 canvas.drawBitmap(STATE_CALENDAR_INVITE, mCoordinates.replyStateX,
1238                         mCoordinates.replyStateY, null);
1239             }
1240         }
1241 
1242         if (mConfig.isPersonalIndicatorVisible()) {
1243             canvas.drawBitmap(mHeader.personalLevelBitmap, mCoordinates.personalIndicatorX,
1244                     mCoordinates.personalIndicatorY, null);
1245         }
1246 
1247         // Info icon
1248         if (mHeader.infoIcon != null) {
1249             canvas.drawBitmap(mHeader.infoIcon, mInfoIconX, mCoordinates.infoIconY, sPaint);
1250         }
1251 
1252         // Date.
1253         sPaint.setTextSize(mCoordinates.dateFontSize);
1254         sPaint.setTypeface(isUnread ? SANS_SERIF_BOLD : SANS_SERIF_LIGHT);
1255         sPaint.setColor(isUnread ? sDateTextColorUnread : sDateTextColorRead);
1256         drawText(canvas, mHeader.dateText, mDateX, mCoordinates.dateYBaseline, sPaint);
1257 
1258         // Paper clip icon.
1259         if (mHeader.paperclip != null) {
1260             canvas.drawBitmap(mHeader.paperclip, mPaperclipX, mCoordinates.paperclipY, sPaint);
1261         }
1262 
1263         // Star.
1264         if (mStarEnabled) {
1265             canvas.drawBitmap(getStarBitmap(), mCoordinates.starX, mCoordinates.starY, sPaint);
1266         }
1267 
1268         // Divider.
1269         if (mDividerEnabled) {
1270             final int dividerBottomY = getHeight();
1271             final int dividerTopY = dividerBottomY - sDividerHeight;
1272             canvas.drawRect(0, dividerTopY, getWidth(), dividerBottomY, sDividerPaint);
1273         }
1274 
1275         // The focused bar
1276         final SwipeableListView listView = getListView();
1277         if (listView != null && listView.isConversationSelected(getConversation())) {
1278             final int w = FOCUSED_CONVERSATION_HIGHLIGHT.getIntrinsicWidth();
1279             final boolean isRtl = ViewUtils.isViewRtl(this);
1280             // This bar is on the right side of the conv list if it's RTL
1281             FOCUSED_CONVERSATION_HIGHLIGHT.setBounds(
1282                     (isRtl) ? getWidth() - w : 0, 0,
1283                     (isRtl) ? getWidth() : w, getHeight());
1284             FOCUSED_CONVERSATION_HIGHLIGHT.draw(canvas);
1285         }
1286 
1287         Utils.traceEndSection();
1288     }
1289 
1290     @Override
setSelected(boolean selected)1291     public void setSelected(boolean selected) {
1292         // We catch the selected event here instead of using ListView#setOnItemSelectedListener
1293         // because when the framework changes selection due to keyboard events, it sets the selected
1294         // state, re-draw the affected views, and then call onItemSelected. That approach won't work
1295         // because the view won't know about the new selected position during the re-draw.
1296         if (selected) {
1297             final SwipeableListView listView = getListView();
1298             if (listView != null) {
1299                 listView.setSelectedConversation(getConversation());
1300             }
1301         }
1302         super.setSelected(selected);
1303     }
1304 
drawSendersImage(final Canvas canvas)1305     private void drawSendersImage(final Canvas canvas) {
1306         if (!mSendersImageView.isFlipping()) {
1307             final boolean showSenders = !mChecked;
1308             mSendersImageView.reset(showSenders);
1309         }
1310         canvas.translate(mCoordinates.contactImagesX, mCoordinates.contactImagesY);
1311         if (mPhotoBitmap == null) {
1312             mSendersImageView.draw(canvas);
1313         } else {
1314             canvas.drawBitmap(mPhotoBitmap, null, mPhotoRect, sPaint);
1315         }
1316     }
1317 
drawSubject(Canvas canvas)1318     private void drawSubject(Canvas canvas) {
1319         canvas.translate(mCoordinates.subjectX, mCoordinates.subjectY);
1320         mSubjectTextView.draw(canvas);
1321     }
1322 
drawSnippet(Canvas canvas)1323     private void drawSnippet(Canvas canvas) {
1324         // if folders exist, their width will be the max width - actual width
1325         final int folderWidth = mCoordinates.maxSnippetWidth - mSnippetTextView.getWidth();
1326 
1327         // in RTL layouts we move the snippet to the right so it doesn't overlap the folders
1328         final int x = mCoordinates.snippetX + (ViewUtils.isViewRtl(this) ? folderWidth : 0);
1329         canvas.translate(x, mCoordinates.snippetY);
1330         mSnippetTextView.draw(canvas);
1331     }
1332 
drawSenders(Canvas canvas)1333     private void drawSenders(Canvas canvas) {
1334         canvas.translate(mSendersX, mCoordinates.sendersY);
1335         mSendersTextView.draw(canvas);
1336     }
1337 
getStarBitmap()1338     private Bitmap getStarBitmap() {
1339         return mHeader.conversation.starred ? STAR_ON : STAR_OFF;
1340     }
1341 
drawText(Canvas canvas, CharSequence s, int x, int y, TextPaint paint)1342     private static void drawText(Canvas canvas, CharSequence s, int x, int y, TextPaint paint) {
1343         canvas.drawText(s, 0, s.length(), x, y, paint);
1344     }
1345 
1346     /**
1347      * Set the background for this item based on:
1348      * 1. Read / Unread (unread messages have a lighter background)
1349      * 2. Tablet / Phone
1350      * 3. Checkbox checked / Unchecked (controls CAB color for item)
1351      * 4. Activated / Not activated (controls the blue highlight on tablet)
1352      */
updateBackground()1353     private void updateBackground() {
1354         final int background;
1355         if (mBackgroundOverrideResId > 0) {
1356             background = mBackgroundOverrideResId;
1357         } else {
1358             background = R.drawable.conversation_item_background;
1359         }
1360         setBackgroundResource(background);
1361     }
1362 
1363     @Override
onCreateDrawableState(int extraSpace)1364     protected int[] onCreateDrawableState(int extraSpace) {
1365         final int[] curr = super.onCreateDrawableState(extraSpace + 1);
1366         if (mChecked) {
1367             mergeDrawableStates(curr, CHECKED_STATE);
1368         }
1369         return curr;
1370     }
1371 
setChecked(boolean checked)1372     private void setChecked(boolean checked) {
1373         mChecked = checked;
1374         refreshDrawableState();
1375     }
1376 
1377     @Override
toggleCheckedState()1378     public boolean toggleCheckedState() {
1379         return toggleCheckedState(null);
1380     }
1381 
1382     @Override
toggleCheckedState(final String sourceOpt)1383     public boolean toggleCheckedState(final String sourceOpt) {
1384         if (mHeader != null && mHeader.conversation != null && mCheckedConversationSet != null) {
1385             setChecked(!mChecked);
1386             final Conversation conv = mHeader.conversation;
1387             // Set the list position of this item in the conversation
1388             final SwipeableListView listView = getListView();
1389 
1390             try {
1391                 conv.position = mChecked && listView != null ? listView.getPositionForView(this)
1392                         : Conversation.NO_POSITION;
1393             } catch (final NullPointerException e) {
1394                 // TODO(skennedy) Remove this if we find the root cause b/9527863
1395             }
1396 
1397             if (mCheckedConversationSet.isEmpty()) {
1398                 final String source = (sourceOpt != null) ? sourceOpt : "checkbox";
1399                 Analytics.getInstance().sendEvent("enter_cab_mode", source, null, 0);
1400             }
1401 
1402             mCheckedConversationSet.toggle(conv);
1403             if (mCheckedConversationSet.isEmpty()) {
1404                 listView.commitDestructiveActions(true);
1405             }
1406 
1407             final boolean front = !mChecked;
1408             mSendersImageView.flipTo(front);
1409 
1410             // We update the background after the checked state has changed
1411             // now that we have a selected background asset. Setting the background
1412             // usually waits for a layout pass, but we don't need a full layout,
1413             // just an update to the background.
1414             requestLayout();
1415 
1416             return true;
1417         }
1418 
1419         return false;
1420     }
1421 
1422     @Override
onSetEmpty()1423     public void onSetEmpty() {
1424         mSendersImageView.flipTo(true);
1425     }
1426 
1427     @Override
onSetPopulated(final ConversationCheckedSet set)1428     public void onSetPopulated(final ConversationCheckedSet set) { }
1429 
1430     @Override
onSetChanged(final ConversationCheckedSet set)1431     public void onSetChanged(final ConversationCheckedSet set) { }
1432 
1433     /**
1434      * Toggle the star on this view and update the conversation.
1435      */
toggleStar()1436     public void toggleStar() {
1437         mHeader.conversation.starred = !mHeader.conversation.starred;
1438         Bitmap starBitmap = getStarBitmap();
1439         postInvalidate(mCoordinates.starX, mCoordinates.starY, mCoordinates.starX
1440                 + starBitmap.getWidth(),
1441                 mCoordinates.starY + starBitmap.getHeight());
1442         ConversationCursor cursor = (ConversationCursor) mAdapter.getCursor();
1443         if (cursor != null) {
1444             // TODO(skennedy) What about ads?
1445             cursor.updateBoolean(mHeader.conversation, ConversationColumns.STARRED,
1446                     mHeader.conversation.starred);
1447         }
1448     }
1449 
isTouchInContactPhoto(float x, float y)1450     private boolean isTouchInContactPhoto(float x, float y) {
1451         // Everything before the end edge of contact photo
1452 
1453         final boolean isRtl = ViewUtils.isViewRtl(this);
1454         final int threshold = (isRtl) ? mCoordinates.contactImagesX - sSenderImageTouchSlop :
1455                 mCoordinates.contactImagesX + mCoordinates.contactImagesWidth
1456                 + sSenderImageTouchSlop;
1457 
1458         // Allow touching a little right of the contact photo when we're already in selection mode
1459         final float extra;
1460         if (mCheckedConversationSet == null || mCheckedConversationSet.isEmpty()) {
1461             extra = 0;
1462         } else {
1463             extra = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16,
1464                     getResources().getDisplayMetrics());
1465         }
1466 
1467         return mHeader.gadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO
1468                 && ((isRtl) ? x > (threshold - extra) : x < (threshold + extra));
1469     }
1470 
isTouchInInfoIcon(final float x, final float y)1471     private boolean isTouchInInfoIcon(final float x, final float y) {
1472         if (mHeader.infoIcon == null) {
1473             // We have no info icon
1474             return false;
1475         }
1476 
1477         final boolean isRtl = ViewUtils.isViewRtl(this);
1478         // Regardless of device, we always want to be end of the date's start touch slop
1479         if (((isRtl) ? x > mDateX + mDateWidth + sStarTouchSlop : x < mDateX - sStarTouchSlop)) {
1480             return false;
1481         }
1482 
1483         if (mStarEnabled) {
1484             // We allow touches all the way to the right edge, so no x check is necessary
1485 
1486             // We need to be above the star's touch area, which ends at the top of the subject
1487             // text
1488             return y < mCoordinates.subjectY;
1489         }
1490 
1491         // With no star below the info icon, we allow touches anywhere from the top edge to the
1492         // bottom edge
1493         return true;
1494     }
1495 
isTouchInStar(float x, float y)1496     private boolean isTouchInStar(float x, float y) {
1497         if (mHeader.infoIcon != null) {
1498             // We have an info icon, and it's above the star
1499             // We allow touches everywhere below the top of the subject text
1500             if (y < mCoordinates.subjectY) {
1501                 return false;
1502             }
1503         }
1504 
1505         // Everything after the star and include a touch slop.
1506         return mStarEnabled && isTouchInStarTargetX(ViewUtils.isViewRtl(this), x);
1507     }
1508 
isTouchInStarTargetX(boolean isRtl, float x)1509     private boolean isTouchInStarTargetX(boolean isRtl, float x) {
1510         return (isRtl) ? x < mCoordinates.starX + mCoordinates.starWidth + sStarTouchSlop
1511                 : x >= mCoordinates.starX - sStarTouchSlop;
1512     }
1513 
1514     @Override
canChildBeDismissed()1515     public boolean canChildBeDismissed() {
1516         return mSwipeEnabled;
1517     }
1518 
1519     @Override
dismiss()1520     public void dismiss() {
1521         SwipeableListView listView = getListView();
1522         if (listView != null) {
1523             listView.dismissChild(this);
1524         }
1525     }
1526 
onTouchEventNoSwipe(MotionEvent event)1527     private boolean onTouchEventNoSwipe(MotionEvent event) {
1528         Utils.traceBeginSection("on touch event no swipe");
1529         boolean handled = false;
1530 
1531         int x = (int) event.getX();
1532         int y = (int) event.getY();
1533         switch (event.getAction()) {
1534             case MotionEvent.ACTION_DOWN:
1535                 if (isTouchInContactPhoto(x, y) || isTouchInInfoIcon(x, y) || isTouchInStar(x, y)) {
1536                     mDownEvent = true;
1537                     handled = true;
1538                 }
1539                 break;
1540 
1541             case MotionEvent.ACTION_CANCEL:
1542                 mDownEvent = false;
1543                 break;
1544 
1545             case MotionEvent.ACTION_UP:
1546                 if (mDownEvent) {
1547                     if (isTouchInContactPhoto(x, y)) {
1548                         // Touch on the check mark
1549                         toggleCheckedState();
1550                     } else if (isTouchInInfoIcon(x, y)) {
1551                         if (mConversationItemAreaClickListener != null) {
1552                             mConversationItemAreaClickListener.onInfoIconClicked();
1553                         }
1554                     } else if (isTouchInStar(x, y)) {
1555                         // Touch on the star
1556                         if (mConversationItemAreaClickListener == null) {
1557                             toggleStar();
1558                         } else {
1559                             mConversationItemAreaClickListener.onStarClicked();
1560                         }
1561                     }
1562                     handled = true;
1563                 }
1564                 break;
1565         }
1566 
1567         if (!handled) {
1568             handled = super.onTouchEvent(event);
1569         }
1570 
1571         Utils.traceEndSection();
1572         return handled;
1573     }
1574 
1575     /**
1576      * ConversationItemView is given the first chance to handle touch events.
1577      */
1578     @Override
onTouchEvent(MotionEvent event)1579     public boolean onTouchEvent(MotionEvent event) {
1580         Utils.traceBeginSection("on touch event");
1581         int x = (int) event.getX();
1582         int y = (int) event.getY();
1583         if (!mSwipeEnabled) {
1584             Utils.traceEndSection();
1585             return onTouchEventNoSwipe(event);
1586         }
1587         switch (event.getAction()) {
1588             case MotionEvent.ACTION_DOWN:
1589                 if (isTouchInContactPhoto(x, y) || isTouchInInfoIcon(x, y) || isTouchInStar(x, y)) {
1590                     mDownEvent = true;
1591                     Utils.traceEndSection();
1592                     return true;
1593                 }
1594                 break;
1595             case MotionEvent.ACTION_UP:
1596                 if (mDownEvent) {
1597                     if (isTouchInContactPhoto(x, y)) {
1598                         // Touch on the check mark
1599                         Utils.traceEndSection();
1600                         mDownEvent = false;
1601                         toggleCheckedState();
1602                         Utils.traceEndSection();
1603                         return true;
1604                     } else if (isTouchInInfoIcon(x, y)) {
1605                         // Touch on the info icon
1606                         mDownEvent = false;
1607                         if (mConversationItemAreaClickListener != null) {
1608                             mConversationItemAreaClickListener.onInfoIconClicked();
1609                         }
1610                         Utils.traceEndSection();
1611                         return true;
1612                     } else if (isTouchInStar(x, y)) {
1613                         // Touch on the star
1614                         mDownEvent = false;
1615                         if (mConversationItemAreaClickListener == null) {
1616                             toggleStar();
1617                         } else {
1618                             mConversationItemAreaClickListener.onStarClicked();
1619                         }
1620                         Utils.traceEndSection();
1621                         return true;
1622                     }
1623                 }
1624                 break;
1625         }
1626         // Let View try to handle it as well.
1627         boolean handled = super.onTouchEvent(event);
1628         if (event.getAction() == MotionEvent.ACTION_DOWN) {
1629             Utils.traceEndSection();
1630             return true;
1631         }
1632         Utils.traceEndSection();
1633         return handled;
1634     }
1635 
1636     @Override
performClick()1637     public boolean performClick() {
1638         final boolean handled = super.performClick();
1639         final SwipeableListView list = getListView();
1640         if (!handled && list != null && list.getAdapter() != null) {
1641             final int pos = list.findConversation(this, mHeader.conversation);
1642             list.performItemClick(this, pos, mHeader.conversation.id);
1643         }
1644         return handled;
1645     }
1646 
unwrap()1647     private View unwrap() {
1648         final ViewParent vp = getParent();
1649         if (vp == null || !(vp instanceof View)) {
1650             return null;
1651         }
1652         return (View) vp;
1653     }
1654 
getListView()1655     private SwipeableListView getListView() {
1656         SwipeableListView v = null;
1657         final View wrapper = unwrap();
1658         if (wrapper != null && wrapper instanceof SwipeableConversationItemView) {
1659             v = (SwipeableListView) ((SwipeableConversationItemView) wrapper).getListView();
1660         }
1661         if (v == null) {
1662             v = mAdapter.getListView();
1663         }
1664         return v;
1665     }
1666 
1667     /**
1668      * Reset any state associated with this conversation item view so that it
1669      * can be reused.
1670      */
reset()1671     public void reset() {
1672         Utils.traceBeginSection("reset");
1673         setAlpha(1f);
1674         setTranslationX(0f);
1675         mAnimatedHeightFraction = 1.0f;
1676         Utils.traceEndSection();
1677     }
1678 
1679     @SuppressWarnings("deprecation")
1680     @Override
setTranslationX(float translationX)1681     public void setTranslationX(float translationX) {
1682         super.setTranslationX(translationX);
1683 
1684         // When a list item is being swiped or animated, ensure that the hosting view has a
1685         // background color set. We only enable the background during the X-translation effect to
1686         // reduce overdraw during normal list scrolling.
1687         final View parent = (View) getParent();
1688         if (parent == null) {
1689             LogUtils.w(LOG_TAG, "CIV.setTranslationX null ConversationItemView parent x=%s",
1690                     translationX);
1691         }
1692 
1693         if (parent instanceof SwipeableConversationItemView) {
1694             if (translationX != 0f) {
1695                 parent.setBackgroundResource(R.color.swiped_bg_color);
1696             } else {
1697                 parent.setBackgroundDrawable(null);
1698             }
1699         }
1700     }
1701 
1702     /**
1703      * Grow the height of the item and fade it in when bringing a conversation
1704      * back from a destructive action.
1705      */
createSwipeUndoAnimation()1706     public Animator createSwipeUndoAnimation() {
1707         ObjectAnimator undoAnimator = createTranslateXAnimation(true);
1708         return undoAnimator;
1709     }
1710 
1711     /**
1712      * Grow the height of the item and fade it in when bringing a conversation
1713      * back from a destructive action.
1714      */
createUndoAnimation()1715     public Animator createUndoAnimation() {
1716         ObjectAnimator height = createHeightAnimation(true);
1717         Animator fade = ObjectAnimator.ofFloat(this, "alpha", 0, 1.0f);
1718         fade.setDuration(sShrinkAnimationDuration);
1719         fade.setInterpolator(new DecelerateInterpolator(2.0f));
1720         AnimatorSet transitionSet = new AnimatorSet();
1721         transitionSet.playTogether(height, fade);
1722         transitionSet.addListener(new HardwareLayerEnabler(this));
1723         return transitionSet;
1724     }
1725 
1726     /**
1727      * Grow the height of the item and fade it in when bringing a conversation
1728      * back from a destructive action.
1729      */
createDestroyWithSwipeAnimation()1730     public Animator createDestroyWithSwipeAnimation() {
1731         ObjectAnimator slide = createTranslateXAnimation(false);
1732         ObjectAnimator height = createHeightAnimation(false);
1733         AnimatorSet transitionSet = new AnimatorSet();
1734         transitionSet.playSequentially(slide, height);
1735         return transitionSet;
1736     }
1737 
createTranslateXAnimation(boolean show)1738     private ObjectAnimator createTranslateXAnimation(boolean show) {
1739         SwipeableListView parent = getListView();
1740         // If we can't get the parent...we have bigger problems.
1741         int width = parent != null ? parent.getMeasuredWidth() : 0;
1742         final float start = show ? width : 0f;
1743         final float end = show ? 0f : width;
1744         ObjectAnimator slide = ObjectAnimator.ofFloat(this, "translationX", start, end);
1745         slide.setInterpolator(new DecelerateInterpolator(2.0f));
1746         slide.setDuration(sSlideAnimationDuration);
1747         return slide;
1748     }
1749 
createDestroyAnimation()1750     public Animator createDestroyAnimation() {
1751         return createHeightAnimation(false);
1752     }
1753 
createHeightAnimation(boolean show)1754     private ObjectAnimator createHeightAnimation(boolean show) {
1755         final float start = show ? 0f : 1.0f;
1756         final float end = show ? 1.0f : 0f;
1757         ObjectAnimator height = ObjectAnimator.ofFloat(this, "animatedHeightFraction", start, end);
1758         height.setInterpolator(new DecelerateInterpolator(2.0f));
1759         height.setDuration(sShrinkAnimationDuration);
1760         return height;
1761     }
1762 
1763     // Used by animator
setAnimatedHeightFraction(float height)1764     public void setAnimatedHeightFraction(float height) {
1765         mAnimatedHeightFraction = height;
1766         requestLayout();
1767     }
1768 
1769     @Override
getSwipeableView()1770     public SwipeableView getSwipeableView() {
1771         return SwipeableView.from(this);
1772     }
1773 
1774     @Override
getMinAllowScrollDistance()1775     public float getMinAllowScrollDistance() {
1776         return sScrollSlop;
1777     }
1778 
getAccountEmailAddress()1779     public String getAccountEmailAddress() {
1780         return mAccount.getEmailAddress();
1781     }
1782 }
1783