ConversationItemView.java revision 2cff0881131d3cd4469d3494a3f7bf0ee3f2f09e
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.Context; 25import android.content.Intent; 26import android.content.IntentFilter; 27import android.content.res.Resources; 28import android.graphics.Bitmap; 29import android.graphics.BitmapFactory; 30import android.graphics.Canvas; 31import android.graphics.Color; 32import android.graphics.Paint; 33import android.graphics.Rect; 34import android.graphics.Typeface; 35import android.graphics.drawable.Drawable; 36import android.graphics.drawable.InsetDrawable; 37import android.support.annotation.Nullable; 38import android.support.v4.text.BidiFormatter; 39import android.support.v4.text.TextUtilsCompat; 40import android.support.v4.view.ViewCompat; 41import android.text.Layout.Alignment; 42import android.text.Spannable; 43import android.text.SpannableString; 44import android.text.SpannableStringBuilder; 45import android.text.StaticLayout; 46import android.text.TextPaint; 47import android.text.TextUtils; 48import android.text.TextUtils.TruncateAt; 49import android.text.format.DateUtils; 50import android.text.style.BackgroundColorSpan; 51import android.text.style.CharacterStyle; 52import android.text.style.ForegroundColorSpan; 53import android.text.style.TextAppearanceSpan; 54import android.util.SparseArray; 55import android.util.TypedValue; 56import android.view.MotionEvent; 57import android.view.View; 58import android.view.ViewGroup; 59import android.view.ViewParent; 60import android.view.animation.DecelerateInterpolator; 61import android.widget.TextView; 62 63import com.android.mail.R; 64import com.android.mail.analytics.Analytics; 65import com.android.mail.bitmap.CheckableContactFlipDrawable; 66import com.android.mail.bitmap.ContactDrawable; 67import com.android.mail.perf.Timer; 68import com.android.mail.providers.Account; 69import com.android.mail.providers.Conversation; 70import com.android.mail.providers.Folder; 71import com.android.mail.providers.UIProvider; 72import com.android.mail.providers.UIProvider.ConversationColumns; 73import com.android.mail.providers.UIProvider.ConversationListIcon; 74import com.android.mail.providers.UIProvider.FolderType; 75import com.android.mail.ui.AnimatedAdapter; 76import com.android.mail.ui.ControllableActivity; 77import com.android.mail.ui.ConversationCheckedSet; 78import com.android.mail.ui.ConversationSetObserver; 79import com.android.mail.ui.FolderDisplayer; 80import com.android.mail.ui.SwipeableItemView; 81import com.android.mail.ui.SwipeableListView; 82import com.android.mail.utils.FolderUri; 83import com.android.mail.utils.HardwareLayerEnabler; 84import com.android.mail.utils.LogTag; 85import com.android.mail.utils.LogUtils; 86import com.android.mail.utils.Utils; 87import com.android.mail.utils.ViewUtils; 88import com.google.common.annotations.VisibleForTesting; 89 90import java.util.List; 91import java.util.Locale; 92 93public 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 VISIBLE_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. */ 221 void onInfoIconClicked(); 222 223 /** Called when the star is clicked. */ 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 244 public ConversationItemFolderDisplayer(Context context, BidiFormatter formatter) { 245 super(context); 246 mFormatter = formatter; 247 } 248 249 @Override 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 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 269 public void reset() { 270 super.reset(); 271 mFoldersCount = 0; 272 } 273 274 public boolean hasVisibleFolders() { 275 return mFoldersCount > 0; 276 } 277 278 /** 279 * @return how much total space the folders list requires. 280 */ 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 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 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 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 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 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 VISIBLE_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 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 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 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 623 protected void onDetachedFromWindow() { 624 super.onDetachedFromWindow(); 625 626 if (mCheckedConversationSet != null) { 627 mCheckedConversationSet.removeObserver(this); 628 } 629 } 630 631 @Override 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 */ 650 public Conversation getConversation() { 651 return mHeader.conversation; 652 } 653 654 private static void startTimer(String tag) { 655 if (sTimer != null) { 656 sTimer.start(tag); 657 } 658 } 659 660 private static void pauseTimer(String tag) { 661 if (sTimer != null) { 662 sTimer.pause(tag); 663 } 664 } 665 666 @Override 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 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 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 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 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. 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 849 private static int makeExactSpecForSize(int size) { 850 return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY); 851 } 852 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 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 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 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 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 942 public int getHorizontalPadding() { 943 return sBadgePaddingExtraWidth; 944 } 945 946 @Override 947 public float getRoundedCornerRadius() { 948 return sBadgeRoundedCornerRadius; 949 } 950 951 // END BadgeSpan.BadgeSpanDimensions override 952 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 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 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 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 */ 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 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 if (isSelected() || isActivated()) { 1277 final int w = VISIBLE_CONVERSATION_HIGHLIGHT.getIntrinsicWidth(); 1278 final boolean isRtl = ViewUtils.isViewRtl(this); 1279 // This bar is on the right side of the conv list if it's RTL 1280 VISIBLE_CONVERSATION_HIGHLIGHT.setBounds( 1281 (isRtl) ? getWidth() - w : 0, 0, 1282 (isRtl) ? getWidth() : w, getHeight()); 1283 VISIBLE_CONVERSATION_HIGHLIGHT.draw(canvas); 1284 } 1285 1286 Utils.traceEndSection(); 1287 } 1288 1289 private void drawSendersImage(final Canvas canvas) { 1290 if (!mSendersImageView.isFlipping()) { 1291 final boolean showSenders = !mChecked; 1292 mSendersImageView.reset(showSenders); 1293 } 1294 canvas.translate(mCoordinates.contactImagesX, mCoordinates.contactImagesY); 1295 if (mPhotoBitmap == null) { 1296 mSendersImageView.draw(canvas); 1297 } else { 1298 canvas.drawBitmap(mPhotoBitmap, null, mPhotoRect, sPaint); 1299 } 1300 } 1301 1302 private void drawSubject(Canvas canvas) { 1303 canvas.translate(mCoordinates.subjectX, mCoordinates.subjectY); 1304 mSubjectTextView.draw(canvas); 1305 } 1306 1307 private void drawSnippet(Canvas canvas) { 1308 // if folders exist, their width will be the max width - actual width 1309 final int folderWidth = mCoordinates.maxSnippetWidth - mSnippetTextView.getWidth(); 1310 1311 // in RTL layouts we move the snippet to the right so it doesn't overlap the folders 1312 final int x = mCoordinates.snippetX + (ViewUtils.isViewRtl(this) ? folderWidth : 0); 1313 canvas.translate(x, mCoordinates.snippetY); 1314 mSnippetTextView.draw(canvas); 1315 } 1316 1317 private void drawSenders(Canvas canvas) { 1318 canvas.translate(mSendersX, mCoordinates.sendersY); 1319 mSendersTextView.draw(canvas); 1320 } 1321 1322 private Bitmap getStarBitmap() { 1323 return mHeader.conversation.starred ? STAR_ON : STAR_OFF; 1324 } 1325 1326 private static void drawText(Canvas canvas, CharSequence s, int x, int y, TextPaint paint) { 1327 canvas.drawText(s, 0, s.length(), x, y, paint); 1328 } 1329 1330 /** 1331 * Set the background for this item based on: 1332 * 1. Read / Unread (unread messages have a lighter background) 1333 * 2. Tablet / Phone 1334 * 3. Checkbox checked / Unchecked (controls CAB color for item) 1335 * 4. Activated / Not activated (controls the blue highlight on tablet) 1336 */ 1337 private void updateBackground() { 1338 final int background; 1339 if (mBackgroundOverrideResId > 0) { 1340 background = mBackgroundOverrideResId; 1341 } else { 1342 background = R.drawable.conversation_item_background; 1343 } 1344 setBackgroundResource(background); 1345 } 1346 1347 @Override 1348 protected int[] onCreateDrawableState(int extraSpace) { 1349 final int[] curr = super.onCreateDrawableState(extraSpace + 1); 1350 if (mChecked) { 1351 mergeDrawableStates(curr, CHECKED_STATE); 1352 } 1353 return curr; 1354 } 1355 1356 private void setChecked(boolean checked) { 1357 mChecked = checked; 1358 refreshDrawableState(); 1359 } 1360 1361 @Override 1362 public boolean toggleCheckedState() { 1363 return toggleCheckedState(null); 1364 } 1365 1366 @Override 1367 public boolean toggleCheckedState(final String sourceOpt) { 1368 if (mHeader != null && mHeader.conversation != null && mCheckedConversationSet != null) { 1369 setChecked(!mChecked); 1370 final Conversation conv = mHeader.conversation; 1371 // Set the list position of this item in the conversation 1372 final SwipeableListView listView = getListView(); 1373 1374 try { 1375 conv.position = mChecked && listView != null ? listView.getPositionForView(this) 1376 : Conversation.NO_POSITION; 1377 } catch (final NullPointerException e) { 1378 // TODO(skennedy) Remove this if we find the root cause b/9527863 1379 } 1380 1381 if (mCheckedConversationSet.isEmpty()) { 1382 final String source = (sourceOpt != null) ? sourceOpt : "checkbox"; 1383 Analytics.getInstance().sendEvent("enter_cab_mode", source, null, 0); 1384 } 1385 1386 mCheckedConversationSet.toggle(conv); 1387 if (mCheckedConversationSet.isEmpty()) { 1388 listView.commitDestructiveActions(true); 1389 } 1390 1391 final boolean front = !mChecked; 1392 mSendersImageView.flipTo(front); 1393 1394 // We update the background after the checked state has changed 1395 // now that we have a selected background asset. Setting the background 1396 // usually waits for a layout pass, but we don't need a full layout, 1397 // just an update to the background. 1398 requestLayout(); 1399 1400 return true; 1401 } 1402 1403 return false; 1404 } 1405 1406 @Override 1407 public void onSetEmpty() { 1408 mSendersImageView.flipTo(true); 1409 } 1410 1411 @Override 1412 public void onSetPopulated(final ConversationCheckedSet set) { } 1413 1414 @Override 1415 public void onSetChanged(final ConversationCheckedSet set) { } 1416 1417 /** 1418 * Toggle the star on this view and update the conversation. 1419 */ 1420 public void toggleStar() { 1421 mHeader.conversation.starred = !mHeader.conversation.starred; 1422 Bitmap starBitmap = getStarBitmap(); 1423 postInvalidate(mCoordinates.starX, mCoordinates.starY, mCoordinates.starX 1424 + starBitmap.getWidth(), 1425 mCoordinates.starY + starBitmap.getHeight()); 1426 ConversationCursor cursor = (ConversationCursor) mAdapter.getCursor(); 1427 if (cursor != null) { 1428 // TODO(skennedy) What about ads? 1429 cursor.updateBoolean(mHeader.conversation, ConversationColumns.STARRED, 1430 mHeader.conversation.starred); 1431 } 1432 } 1433 1434 private boolean isTouchInContactPhoto(float x, float y) { 1435 // Everything before the end edge of contact photo 1436 1437 final boolean isRtl = ViewUtils.isViewRtl(this); 1438 final int threshold = (isRtl) ? mCoordinates.contactImagesX - sSenderImageTouchSlop : 1439 mCoordinates.contactImagesX + mCoordinates.contactImagesWidth 1440 + sSenderImageTouchSlop; 1441 1442 // Allow touching a little right of the contact photo when we're already in selection mode 1443 final float extra; 1444 if (mCheckedConversationSet == null || mCheckedConversationSet.isEmpty()) { 1445 extra = 0; 1446 } else { 1447 extra = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, 1448 getResources().getDisplayMetrics()); 1449 } 1450 1451 return mHeader.gadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO 1452 && ((isRtl) ? x > (threshold - extra) : x < (threshold + extra)); 1453 } 1454 1455 private boolean isTouchInInfoIcon(final float x, final float y) { 1456 if (mHeader.infoIcon == null) { 1457 // We have no info icon 1458 return false; 1459 } 1460 1461 final boolean isRtl = ViewUtils.isViewRtl(this); 1462 // Regardless of device, we always want to be end of the date's start touch slop 1463 if (((isRtl) ? x > mDateX + mDateWidth + sStarTouchSlop : x < mDateX - sStarTouchSlop)) { 1464 return false; 1465 } 1466 1467 if (mStarEnabled) { 1468 // We allow touches all the way to the right edge, so no x check is necessary 1469 1470 // We need to be above the star's touch area, which ends at the top of the subject 1471 // text 1472 return y < mCoordinates.subjectY; 1473 } 1474 1475 // With no star below the info icon, we allow touches anywhere from the top edge to the 1476 // bottom edge 1477 return true; 1478 } 1479 1480 private boolean isTouchInStar(float x, float y) { 1481 if (mHeader.infoIcon != null) { 1482 // We have an info icon, and it's above the star 1483 // We allow touches everywhere below the top of the subject text 1484 if (y < mCoordinates.subjectY) { 1485 return false; 1486 } 1487 } 1488 1489 // Everything after the star and include a touch slop. 1490 return mStarEnabled && isTouchInStarTargetX(ViewUtils.isViewRtl(this), x); 1491 } 1492 1493 private boolean isTouchInStarTargetX(boolean isRtl, float x) { 1494 return (isRtl) ? x < mCoordinates.starX + mCoordinates.starWidth + sStarTouchSlop 1495 : x >= mCoordinates.starX - sStarTouchSlop; 1496 } 1497 1498 @Override 1499 public boolean canChildBeDismissed() { 1500 return mSwipeEnabled; 1501 } 1502 1503 @Override 1504 public void dismiss() { 1505 SwipeableListView listView = getListView(); 1506 if (listView != null) { 1507 listView.dismissChild(this); 1508 } 1509 } 1510 1511 private boolean onTouchEventNoSwipe(MotionEvent event) { 1512 Utils.traceBeginSection("on touch event no swipe"); 1513 boolean handled = false; 1514 1515 int x = (int) event.getX(); 1516 int y = (int) event.getY(); 1517 switch (event.getAction()) { 1518 case MotionEvent.ACTION_DOWN: 1519 if (isTouchInContactPhoto(x, y) || isTouchInInfoIcon(x, y) || isTouchInStar(x, y)) { 1520 mDownEvent = true; 1521 handled = true; 1522 } 1523 break; 1524 1525 case MotionEvent.ACTION_CANCEL: 1526 mDownEvent = false; 1527 break; 1528 1529 case MotionEvent.ACTION_UP: 1530 if (mDownEvent) { 1531 if (isTouchInContactPhoto(x, y)) { 1532 // Touch on the check mark 1533 toggleCheckedState(); 1534 } else if (isTouchInInfoIcon(x, y)) { 1535 if (mConversationItemAreaClickListener != null) { 1536 mConversationItemAreaClickListener.onInfoIconClicked(); 1537 } 1538 } else if (isTouchInStar(x, y)) { 1539 // Touch on the star 1540 if (mConversationItemAreaClickListener == null) { 1541 toggleStar(); 1542 } else { 1543 mConversationItemAreaClickListener.onStarClicked(); 1544 } 1545 } 1546 handled = true; 1547 } 1548 break; 1549 } 1550 1551 if (!handled) { 1552 handled = super.onTouchEvent(event); 1553 } 1554 1555 Utils.traceEndSection(); 1556 return handled; 1557 } 1558 1559 /** 1560 * ConversationItemView is given the first chance to handle touch events. 1561 */ 1562 @Override 1563 public boolean onTouchEvent(MotionEvent event) { 1564 Utils.traceBeginSection("on touch event"); 1565 int x = (int) event.getX(); 1566 int y = (int) event.getY(); 1567 if (!mSwipeEnabled) { 1568 Utils.traceEndSection(); 1569 return onTouchEventNoSwipe(event); 1570 } 1571 switch (event.getAction()) { 1572 case MotionEvent.ACTION_DOWN: 1573 if (isTouchInContactPhoto(x, y) || isTouchInInfoIcon(x, y) || isTouchInStar(x, y)) { 1574 mDownEvent = true; 1575 Utils.traceEndSection(); 1576 return true; 1577 } 1578 break; 1579 case MotionEvent.ACTION_UP: 1580 if (mDownEvent) { 1581 if (isTouchInContactPhoto(x, y)) { 1582 // Touch on the check mark 1583 Utils.traceEndSection(); 1584 mDownEvent = false; 1585 toggleCheckedState(); 1586 Utils.traceEndSection(); 1587 return true; 1588 } else if (isTouchInInfoIcon(x, y)) { 1589 // Touch on the info icon 1590 mDownEvent = false; 1591 if (mConversationItemAreaClickListener != null) { 1592 mConversationItemAreaClickListener.onInfoIconClicked(); 1593 } 1594 Utils.traceEndSection(); 1595 return true; 1596 } else if (isTouchInStar(x, y)) { 1597 // Touch on the star 1598 mDownEvent = false; 1599 if (mConversationItemAreaClickListener == null) { 1600 toggleStar(); 1601 } else { 1602 mConversationItemAreaClickListener.onStarClicked(); 1603 } 1604 Utils.traceEndSection(); 1605 return true; 1606 } 1607 } 1608 break; 1609 } 1610 // Let View try to handle it as well. 1611 boolean handled = super.onTouchEvent(event); 1612 if (event.getAction() == MotionEvent.ACTION_DOWN) { 1613 Utils.traceEndSection(); 1614 return true; 1615 } 1616 Utils.traceEndSection(); 1617 return handled; 1618 } 1619 1620 @Override 1621 public boolean performClick() { 1622 final boolean handled = super.performClick(); 1623 final SwipeableListView list = getListView(); 1624 if (!handled && list != null && list.getAdapter() != null) { 1625 final int pos = list.findConversation(this, mHeader.conversation); 1626 list.performItemClick(this, pos, mHeader.conversation.id); 1627 } 1628 return handled; 1629 } 1630 1631 private View unwrap() { 1632 final ViewParent vp = getParent(); 1633 if (vp == null || !(vp instanceof View)) { 1634 return null; 1635 } 1636 return (View) vp; 1637 } 1638 1639 private SwipeableListView getListView() { 1640 SwipeableListView v = null; 1641 final View wrapper = unwrap(); 1642 if (wrapper != null && wrapper instanceof SwipeableConversationItemView) { 1643 v = (SwipeableListView) ((SwipeableConversationItemView) wrapper).getListView(); 1644 } 1645 if (v == null) { 1646 v = mAdapter.getListView(); 1647 } 1648 return v; 1649 } 1650 1651 /** 1652 * Reset any state associated with this conversation item view so that it 1653 * can be reused. 1654 */ 1655 public void reset() { 1656 Utils.traceBeginSection("reset"); 1657 setAlpha(1f); 1658 setTranslationX(0f); 1659 mAnimatedHeightFraction = 1.0f; 1660 Utils.traceEndSection(); 1661 } 1662 1663 @SuppressWarnings("deprecation") 1664 @Override 1665 public void setTranslationX(float translationX) { 1666 super.setTranslationX(translationX); 1667 1668 // When a list item is being swiped or animated, ensure that the hosting view has a 1669 // background color set. We only enable the background during the X-translation effect to 1670 // reduce overdraw during normal list scrolling. 1671 final View parent = (View) getParent(); 1672 if (parent == null) { 1673 LogUtils.w(LOG_TAG, "CIV.setTranslationX null ConversationItemView parent x=%s", 1674 translationX); 1675 } 1676 1677 if (parent instanceof SwipeableConversationItemView) { 1678 if (translationX != 0f) { 1679 parent.setBackgroundResource(R.color.swiped_bg_color); 1680 } else { 1681 parent.setBackgroundDrawable(null); 1682 } 1683 } 1684 } 1685 1686 /** 1687 * Grow the height of the item and fade it in when bringing a conversation 1688 * back from a destructive action. 1689 */ 1690 public Animator createSwipeUndoAnimation() { 1691 ObjectAnimator undoAnimator = createTranslateXAnimation(true); 1692 return undoAnimator; 1693 } 1694 1695 /** 1696 * Grow the height of the item and fade it in when bringing a conversation 1697 * back from a destructive action. 1698 */ 1699 public Animator createUndoAnimation() { 1700 ObjectAnimator height = createHeightAnimation(true); 1701 Animator fade = ObjectAnimator.ofFloat(this, "alpha", 0, 1.0f); 1702 fade.setDuration(sShrinkAnimationDuration); 1703 fade.setInterpolator(new DecelerateInterpolator(2.0f)); 1704 AnimatorSet transitionSet = new AnimatorSet(); 1705 transitionSet.playTogether(height, fade); 1706 transitionSet.addListener(new HardwareLayerEnabler(this)); 1707 return transitionSet; 1708 } 1709 1710 /** 1711 * Grow the height of the item and fade it in when bringing a conversation 1712 * back from a destructive action. 1713 */ 1714 public Animator createDestroyWithSwipeAnimation() { 1715 ObjectAnimator slide = createTranslateXAnimation(false); 1716 ObjectAnimator height = createHeightAnimation(false); 1717 AnimatorSet transitionSet = new AnimatorSet(); 1718 transitionSet.playSequentially(slide, height); 1719 return transitionSet; 1720 } 1721 1722 private ObjectAnimator createTranslateXAnimation(boolean show) { 1723 SwipeableListView parent = getListView(); 1724 // If we can't get the parent...we have bigger problems. 1725 int width = parent != null ? parent.getMeasuredWidth() : 0; 1726 final float start = show ? width : 0f; 1727 final float end = show ? 0f : width; 1728 ObjectAnimator slide = ObjectAnimator.ofFloat(this, "translationX", start, end); 1729 slide.setInterpolator(new DecelerateInterpolator(2.0f)); 1730 slide.setDuration(sSlideAnimationDuration); 1731 return slide; 1732 } 1733 1734 public Animator createDestroyAnimation() { 1735 return createHeightAnimation(false); 1736 } 1737 1738 private ObjectAnimator createHeightAnimation(boolean show) { 1739 final float start = show ? 0f : 1.0f; 1740 final float end = show ? 1.0f : 0f; 1741 ObjectAnimator height = ObjectAnimator.ofFloat(this, "animatedHeightFraction", start, end); 1742 height.setInterpolator(new DecelerateInterpolator(2.0f)); 1743 height.setDuration(sShrinkAnimationDuration); 1744 return height; 1745 } 1746 1747 // Used by animator 1748 public void setAnimatedHeightFraction(float height) { 1749 mAnimatedHeightFraction = height; 1750 requestLayout(); 1751 } 1752 1753 @Override 1754 public SwipeableView getSwipeableView() { 1755 return SwipeableView.from(this); 1756 } 1757 1758 @Override 1759 public float getMinAllowScrollDistance() { 1760 return sScrollSlop; 1761 } 1762 1763 public String getAccountEmailAddress() { 1764 return mAccount.getEmailAddress(); 1765 } 1766} 1767