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.app.FragmentManager; 21 import android.app.LoaderManager; 22 import android.content.Context; 23 import androidx.annotation.IntDef; 24 import androidx.core.text.BidiFormatter; 25 import android.view.Gravity; 26 import android.view.LayoutInflater; 27 import android.view.View; 28 import android.view.ViewGroup; 29 import android.view.ViewParent; 30 import android.widget.BaseAdapter; 31 32 import com.android.emailcommon.mail.Address; 33 import com.android.mail.ContactInfoSource; 34 import com.android.mail.FormattedDateBuilder; 35 import com.android.mail.R; 36 import com.android.mail.browse.ConversationFooterView.ConversationFooterCallbacks; 37 import com.android.mail.browse.ConversationViewHeader.ConversationViewHeaderCallbacks; 38 import com.android.mail.browse.MessageFooterView.MessageFooterCallbacks; 39 import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks; 40 import com.android.mail.browse.SuperCollapsedBlock.OnClickListener; 41 import com.android.mail.providers.Conversation; 42 import com.android.mail.providers.UIProvider; 43 import com.android.mail.ui.ControllableActivity; 44 import com.android.mail.ui.ConversationUpdater; 45 import com.android.mail.utils.LogTag; 46 import com.android.mail.utils.LogUtils; 47 import com.android.mail.utils.VeiledAddressMatcher; 48 import com.google.common.base.Objects; 49 import com.google.common.collect.Lists; 50 51 import java.lang.annotation.Retention; 52 import java.lang.annotation.RetentionPolicy; 53 import java.util.Collection; 54 import java.util.List; 55 import java.util.Map; 56 import java.util.Set; 57 58 /** 59 * A specialized adapter that contains overlay views to draw on top of the underlying conversation 60 * WebView. Each independently drawn overlay view gets its own item in this adapter, and indices 61 * in this adapter do not necessarily line up with cursor indices. For example, an expanded 62 * message may have a header and footer, and since they are not drawn coupled together, they each 63 * get an adapter item. 64 * <p> 65 * Each item in this adapter is a {@link ConversationOverlayItem} to expose enough information 66 * to {@link ConversationContainer} so that it can position overlays properly. 67 * 68 */ 69 public class ConversationViewAdapter extends BaseAdapter { 70 71 private static final String LOG_TAG = LogTag.getLogTag(); 72 private static final String OVERLAY_ITEM_ROOT_TAG = "overlay_item_root"; 73 74 private final Context mContext; 75 private final FormattedDateBuilder mDateBuilder; 76 private final ConversationAccountController mAccountController; 77 private final LoaderManager mLoaderManager; 78 private final FragmentManager mFragmentManager; 79 private final MessageHeaderViewCallbacks mMessageCallbacks; 80 private final MessageFooterCallbacks mFooterCallbacks; 81 private final ContactInfoSource mContactInfoSource; 82 private final ConversationViewHeaderCallbacks mConversationCallbacks; 83 private final ConversationFooterCallbacks mConversationFooterCallbacks; 84 private final ConversationUpdater mConversationUpdater; 85 private final OnClickListener mSuperCollapsedListener; 86 private final Map<String, Address> mAddressCache; 87 private final LayoutInflater mInflater; 88 89 private final List<ConversationOverlayItem> mItems; 90 private final VeiledAddressMatcher mMatcher; 91 92 @Retention(RetentionPolicy.SOURCE) 93 @IntDef({ 94 VIEW_TYPE_CONVERSATION_HEADER, 95 VIEW_TYPE_CONVERSATION_FOOTER, 96 VIEW_TYPE_MESSAGE_HEADER, 97 VIEW_TYPE_MESSAGE_FOOTER, 98 VIEW_TYPE_SUPER_COLLAPSED_BLOCK, 99 VIEW_TYPE_AD_HEADER, 100 VIEW_TYPE_AD_SENDER_HEADER, 101 VIEW_TYPE_AD_FOOTER 102 }) 103 public @interface ConversationViewType {} 104 public static final int VIEW_TYPE_CONVERSATION_HEADER = 0; 105 public static final int VIEW_TYPE_CONVERSATION_FOOTER = 1; 106 public static final int VIEW_TYPE_MESSAGE_HEADER = 2; 107 public static final int VIEW_TYPE_MESSAGE_FOOTER = 3; 108 public static final int VIEW_TYPE_SUPER_COLLAPSED_BLOCK = 4; 109 public static final int VIEW_TYPE_AD_HEADER = 5; 110 public static final int VIEW_TYPE_AD_SENDER_HEADER = 6; 111 public static final int VIEW_TYPE_AD_FOOTER = 7; 112 public static final int VIEW_TYPE_COUNT = 8; 113 114 private final BidiFormatter mBidiFormatter; 115 116 private final View.OnKeyListener mOnKeyListener; 117 118 public class ConversationHeaderItem extends ConversationOverlayItem { 119 public final Conversation mConversation; 120 ConversationHeaderItem(Conversation conv)121 private ConversationHeaderItem(Conversation conv) { 122 mConversation = conv; 123 } 124 125 @Override getType()126 public @ConversationViewType int getType() { 127 return VIEW_TYPE_CONVERSATION_HEADER; 128 } 129 130 @Override createView(Context context, LayoutInflater inflater, ViewGroup parent)131 public View createView(Context context, LayoutInflater inflater, ViewGroup parent) { 132 final ConversationViewHeader v = (ConversationViewHeader) inflater.inflate( 133 R.layout.conversation_view_header, parent, false); 134 v.setCallbacks( 135 mConversationCallbacks, mAccountController, mConversationUpdater); 136 v.setSubject(mConversation.subject); 137 if (mAccountController.getAccount().supportsCapability( 138 UIProvider.AccountCapabilities.MULTIPLE_FOLDERS_PER_CONV)) { 139 v.setFolders(mConversation); 140 } 141 v.setStarred(mConversation.starred); 142 v.setTag(OVERLAY_ITEM_ROOT_TAG); 143 144 return v; 145 } 146 147 @Override bindView(View v, boolean measureOnly)148 public void bindView(View v, boolean measureOnly) { 149 ConversationViewHeader header = (ConversationViewHeader) v; 150 header.bind(this); 151 } 152 153 @Override isContiguous()154 public boolean isContiguous() { 155 return true; 156 } 157 158 @Override getOnKeyListener()159 public View.OnKeyListener getOnKeyListener() { 160 return mOnKeyListener; 161 } 162 getAdapter()163 public ConversationViewAdapter getAdapter() { 164 return ConversationViewAdapter.this; 165 } 166 } 167 168 public class ConversationFooterItem extends ConversationOverlayItem { 169 private MessageHeaderItem mLastMessageHeaderItem; 170 ConversationFooterItem(MessageHeaderItem lastMessageHeaderItem)171 public ConversationFooterItem(MessageHeaderItem lastMessageHeaderItem) { 172 setLastMessageHeaderItem(lastMessageHeaderItem); 173 } 174 175 @Override getType()176 public @ConversationViewType int getType() { 177 return VIEW_TYPE_CONVERSATION_FOOTER; 178 } 179 180 @Override createView(Context context, LayoutInflater inflater, ViewGroup parent)181 public View createView(Context context, LayoutInflater inflater, ViewGroup parent) { 182 final ConversationFooterView v = (ConversationFooterView) 183 inflater.inflate(R.layout.conversation_footer, parent, false); 184 v.setAccountController(mAccountController); 185 v.setConversationFooterCallbacks(mConversationFooterCallbacks); 186 v.setTag(OVERLAY_ITEM_ROOT_TAG); 187 188 // Register the onkey listener for all relevant views 189 registerOnKeyListeners(v, v.findViewById(R.id.reply_button), 190 v.findViewById(R.id.reply_all_button), v.findViewById(R.id.forward_button)); 191 192 return v; 193 } 194 195 @Override bindView(View v, boolean measureOnly)196 public void bindView(View v, boolean measureOnly) { 197 ((ConversationFooterView) v).bind(this); 198 mRootView = v; 199 } 200 201 @Override rebindView(View view)202 public void rebindView(View view) { 203 ((ConversationFooterView) view).rebind(this); 204 mRootView = view; 205 } 206 207 @Override getFocusableView()208 public View getFocusableView() { 209 return mRootView.findViewById(R.id.reply_button); 210 } 211 212 @Override isContiguous()213 public boolean isContiguous() { 214 return true; 215 } 216 217 @Override getOnKeyListener()218 public View.OnKeyListener getOnKeyListener() { 219 return mOnKeyListener; 220 } 221 getLastMessageHeaderItem()222 public MessageHeaderItem getLastMessageHeaderItem() { 223 return mLastMessageHeaderItem; 224 } 225 setLastMessageHeaderItem(MessageHeaderItem lastMessageHeaderItem)226 public void setLastMessageHeaderItem(MessageHeaderItem lastMessageHeaderItem) { 227 mLastMessageHeaderItem = lastMessageHeaderItem; 228 } 229 } 230 231 public static class MessageHeaderItem extends ConversationOverlayItem { 232 233 private final ConversationViewAdapter mAdapter; 234 235 private ConversationMessage mMessage; 236 237 // view state variables 238 private boolean mExpanded; 239 public boolean detailsExpanded; 240 private boolean mShowImages; 241 242 // cached values to speed up re-rendering during view recycling 243 private CharSequence mTimestampShort; 244 private CharSequence mTimestampLong; 245 private CharSequence mTimestampFull; 246 private long mTimestampMs; 247 private final FormattedDateBuilder mDateBuilder; 248 public CharSequence recipientSummaryText; 249 MessageHeaderItem(ConversationViewAdapter adapter, FormattedDateBuilder dateBuilder, ConversationMessage message, boolean expanded, boolean showImages)250 MessageHeaderItem(ConversationViewAdapter adapter, FormattedDateBuilder dateBuilder, 251 ConversationMessage message, boolean expanded, boolean showImages) { 252 mAdapter = adapter; 253 mDateBuilder = dateBuilder; 254 mMessage = message; 255 mExpanded = expanded; 256 mShowImages = showImages; 257 258 detailsExpanded = false; 259 } 260 getMessage()261 public ConversationMessage getMessage() { 262 return mMessage; 263 } 264 265 @Override getType()266 public @ConversationViewType int getType() { 267 return VIEW_TYPE_MESSAGE_HEADER; 268 } 269 270 @Override createView(Context context, LayoutInflater inflater, ViewGroup parent)271 public View createView(Context context, LayoutInflater inflater, ViewGroup parent) { 272 final MessageHeaderView v = (MessageHeaderView) inflater.inflate( 273 R.layout.conversation_message_header, parent, false); 274 v.initialize(mAdapter.mAccountController, 275 mAdapter.mAddressCache); 276 v.setCallbacks(mAdapter.mMessageCallbacks); 277 v.setContactInfoSource(mAdapter.mContactInfoSource); 278 v.setVeiledMatcher(mAdapter.mMatcher); 279 v.setTag(OVERLAY_ITEM_ROOT_TAG); 280 281 // Register the onkey listener for all relevant views 282 registerOnKeyListeners(v, v.findViewById(R.id.upper_header), 283 v.findViewById(R.id.hide_details), v.findViewById(R.id.edit_draft), 284 v.findViewById(R.id.reply), v.findViewById(R.id.reply_all), 285 v.findViewById(R.id.overflow), v.findViewById(R.id.send_date)); 286 return v; 287 } 288 289 @Override bindView(View v, boolean measureOnly)290 public void bindView(View v, boolean measureOnly) { 291 final MessageHeaderView header = (MessageHeaderView) v; 292 header.bind(this, measureOnly); 293 mRootView = v; 294 } 295 296 @Override getFocusableView()297 public View getFocusableView() { 298 return mRootView.findViewById(R.id.upper_header); 299 } 300 301 @Override onModelUpdated(View v)302 public void onModelUpdated(View v) { 303 final MessageHeaderView header = (MessageHeaderView) v; 304 header.refresh(); 305 } 306 307 @Override isContiguous()308 public boolean isContiguous() { 309 return !isExpanded(); 310 } 311 312 @Override getOnKeyListener()313 public View.OnKeyListener getOnKeyListener() { 314 return mAdapter.getOnKeyListener(); 315 } 316 317 @Override isExpanded()318 public boolean isExpanded() { 319 return mExpanded; 320 } 321 setExpanded(boolean expanded)322 public void setExpanded(boolean expanded) { 323 if (mExpanded != expanded) { 324 mExpanded = expanded; 325 } 326 } 327 getShowImages()328 public boolean getShowImages() { 329 return mShowImages; 330 } 331 setShowImages(boolean showImages)332 public void setShowImages(boolean showImages) { 333 mShowImages = showImages; 334 } 335 336 @Override canBecomeSnapHeader()337 public boolean canBecomeSnapHeader() { 338 return isExpanded(); 339 } 340 341 @Override canPushSnapHeader()342 public boolean canPushSnapHeader() { 343 return true; 344 } 345 346 @Override belongsToMessage(ConversationMessage message)347 public boolean belongsToMessage(ConversationMessage message) { 348 return Objects.equal(mMessage, message); 349 } 350 351 @Override setMessage(ConversationMessage message)352 public void setMessage(ConversationMessage message) { 353 mMessage = message; 354 // setMessage signifies an in-place update to the message, so let's clear out recipient 355 // summary text so the view will refresh it on the next render. 356 recipientSummaryText = null; 357 } 358 getTimestampShort()359 public CharSequence getTimestampShort() { 360 ensureTimestamps(); 361 return mTimestampShort; 362 } 363 getTimestampLong()364 public CharSequence getTimestampLong() { 365 ensureTimestamps(); 366 return mTimestampLong; 367 } 368 getTimestampFull()369 public CharSequence getTimestampFull() { 370 ensureTimestamps(); 371 return mTimestampFull; 372 } 373 ensureTimestamps()374 private void ensureTimestamps() { 375 if (mMessage.dateReceivedMs != mTimestampMs) { 376 mTimestampMs = mMessage.dateReceivedMs; 377 mTimestampShort = mDateBuilder.formatShortDateTime(mTimestampMs); 378 mTimestampLong = mDateBuilder.formatLongDateTime(mTimestampMs); 379 mTimestampFull = mDateBuilder.formatFullDateTime(mTimestampMs); 380 } 381 } 382 getAdapter()383 public ConversationViewAdapter getAdapter() { 384 return mAdapter; 385 } 386 387 @Override rebindView(View view)388 public void rebindView(View view) { 389 final MessageHeaderView header = (MessageHeaderView) view; 390 header.rebind(this); 391 mRootView = view; 392 } 393 } 394 395 public static class MessageFooterItem extends ConversationOverlayItem { 396 private final ConversationViewAdapter mAdapter; 397 398 /** 399 * A footer can only exist if there is a matching header. Requiring a header allows a 400 * footer to stay in sync with the expanded state of the header. 401 */ 402 private final MessageHeaderItem mHeaderItem; 403 MessageFooterItem(ConversationViewAdapter adapter, MessageHeaderItem item)404 private MessageFooterItem(ConversationViewAdapter adapter, MessageHeaderItem item) { 405 mAdapter = adapter; 406 mHeaderItem = item; 407 } 408 409 @Override getType()410 public @ConversationViewType int getType() { 411 return VIEW_TYPE_MESSAGE_FOOTER; 412 } 413 414 @Override createView(Context context, LayoutInflater inflater, ViewGroup parent)415 public View createView(Context context, LayoutInflater inflater, ViewGroup parent) { 416 final MessageFooterView v = (MessageFooterView) inflater.inflate( 417 R.layout.conversation_message_footer, parent, false); 418 v.initialize(mAdapter.mLoaderManager, mAdapter.mFragmentManager, 419 mAdapter.mAccountController, mAdapter.mFooterCallbacks); 420 v.setTag(OVERLAY_ITEM_ROOT_TAG); 421 422 // Register the onkey listener for all relevant views 423 registerOnKeyListeners(v, v.findViewById(R.id.view_entire_message_prompt)); 424 return v; 425 } 426 427 @Override bindView(View v, boolean measureOnly)428 public void bindView(View v, boolean measureOnly) { 429 final MessageFooterView attachmentsView = (MessageFooterView) v; 430 attachmentsView.bind(mHeaderItem, measureOnly); 431 mRootView = v; 432 } 433 434 @Override isContiguous()435 public boolean isContiguous() { 436 return true; 437 } 438 439 @Override getOnKeyListener()440 public View.OnKeyListener getOnKeyListener() { 441 return mAdapter.getOnKeyListener(); 442 } 443 444 @Override isExpanded()445 public boolean isExpanded() { 446 return mHeaderItem.isExpanded(); 447 } 448 449 @Override getGravity()450 public int getGravity() { 451 // attachments are top-aligned within their spacer area 452 // Attachments should stay near the body they belong to, even when zoomed far in. 453 return Gravity.TOP; 454 } 455 456 @Override getHeight()457 public int getHeight() { 458 // a footer may change height while its view does not exist because it is offscreen 459 // (but the header is onscreen and thus collapsible) 460 if (!mHeaderItem.isExpanded()) { 461 return 0; 462 } 463 return super.getHeight(); 464 } 465 getHeaderItem()466 public MessageHeaderItem getHeaderItem() { 467 return mHeaderItem; 468 } 469 } 470 471 public class SuperCollapsedBlockItem extends ConversationOverlayItem { 472 473 private final int mStart; 474 private final int mEnd; 475 private final boolean mHasDraft; 476 SuperCollapsedBlockItem(int start, int end, boolean hasDraft)477 private SuperCollapsedBlockItem(int start, int end, boolean hasDraft) { 478 mStart = start; 479 mEnd = end; 480 mHasDraft = hasDraft; 481 } 482 483 @Override getType()484 public @ConversationViewType int getType() { 485 return VIEW_TYPE_SUPER_COLLAPSED_BLOCK; 486 } 487 488 @Override createView(Context context, LayoutInflater inflater, ViewGroup parent)489 public View createView(Context context, LayoutInflater inflater, ViewGroup parent) { 490 final SuperCollapsedBlock v = (SuperCollapsedBlock) inflater.inflate( 491 R.layout.super_collapsed_block, parent, false); 492 v.initialize(mSuperCollapsedListener); 493 v.setOnKeyListener(mOnKeyListener); 494 v.setTag(OVERLAY_ITEM_ROOT_TAG); 495 496 // Register the onkey listener for all relevant views 497 registerOnKeyListeners(v); 498 return v; 499 } 500 501 @Override bindView(View v, boolean measureOnly)502 public void bindView(View v, boolean measureOnly) { 503 final SuperCollapsedBlock scb = (SuperCollapsedBlock) v; 504 scb.bind(this); 505 mRootView = v; 506 } 507 508 @Override isContiguous()509 public boolean isContiguous() { 510 return true; 511 } 512 513 @Override getOnKeyListener()514 public View.OnKeyListener getOnKeyListener() { 515 return mOnKeyListener; 516 } 517 518 @Override isExpanded()519 public boolean isExpanded() { 520 return false; 521 } 522 getStart()523 public int getStart() { 524 return mStart; 525 } 526 getEnd()527 public int getEnd() { 528 return mEnd; 529 } 530 hasDraft()531 public boolean hasDraft() { 532 return mHasDraft; 533 } 534 535 @Override canPushSnapHeader()536 public boolean canPushSnapHeader() { 537 return true; 538 } 539 } 540 ConversationViewAdapter(ControllableActivity controllableActivity, ConversationAccountController accountController, LoaderManager loaderManager, MessageHeaderViewCallbacks messageCallbacks, MessageFooterCallbacks footerCallbacks, ContactInfoSource contactInfoSource, ConversationViewHeaderCallbacks convCallbacks, ConversationFooterCallbacks convFooterCallbacks, ConversationUpdater conversationUpdater, OnClickListener scbListener, Map<String, Address> addressCache, FormattedDateBuilder dateBuilder, BidiFormatter bidiFormatter, View.OnKeyListener onKeyListener)541 public ConversationViewAdapter(ControllableActivity controllableActivity, 542 ConversationAccountController accountController, 543 LoaderManager loaderManager, 544 MessageHeaderViewCallbacks messageCallbacks, 545 MessageFooterCallbacks footerCallbacks, 546 ContactInfoSource contactInfoSource, 547 ConversationViewHeaderCallbacks convCallbacks, 548 ConversationFooterCallbacks convFooterCallbacks, 549 ConversationUpdater conversationUpdater, 550 OnClickListener scbListener, 551 Map<String, Address> addressCache, 552 FormattedDateBuilder dateBuilder, 553 BidiFormatter bidiFormatter, 554 View.OnKeyListener onKeyListener) { 555 mContext = controllableActivity.getActivityContext(); 556 mDateBuilder = dateBuilder; 557 mAccountController = accountController; 558 mLoaderManager = loaderManager; 559 mFragmentManager = controllableActivity.getFragmentManager(); 560 mMessageCallbacks = messageCallbacks; 561 mFooterCallbacks = footerCallbacks; 562 mContactInfoSource = contactInfoSource; 563 mConversationCallbacks = convCallbacks; 564 mConversationFooterCallbacks = convFooterCallbacks; 565 mConversationUpdater = conversationUpdater; 566 mSuperCollapsedListener = scbListener; 567 mAddressCache = addressCache; 568 mInflater = LayoutInflater.from(mContext); 569 570 mItems = Lists.newArrayList(); 571 mMatcher = controllableActivity.getAccountController().getVeiledAddressMatcher(); 572 573 mBidiFormatter = bidiFormatter; 574 mOnKeyListener = onKeyListener; 575 } 576 577 @Override getCount()578 public int getCount() { 579 return mItems.size(); 580 } 581 582 @Override getItemViewType(int position)583 public @ConversationViewType int getItemViewType(int position) { 584 return mItems.get(position).getType(); 585 } 586 587 @Override getViewTypeCount()588 public int getViewTypeCount() { 589 return VIEW_TYPE_COUNT; 590 } 591 592 @Override getItem(int position)593 public ConversationOverlayItem getItem(int position) { 594 return mItems.get(position); 595 } 596 597 @Override getItemId(int position)598 public long getItemId(int position) { 599 return position; // TODO: ensure this works well enough 600 } 601 602 @Override getView(int position, View convertView, ViewGroup parent)603 public View getView(int position, View convertView, ViewGroup parent) { 604 return getView(getItem(position), convertView, parent, false /* measureOnly */); 605 } 606 getView(ConversationOverlayItem item, View convertView, ViewGroup parent, boolean measureOnly)607 public View getView(ConversationOverlayItem item, View convertView, ViewGroup parent, 608 boolean measureOnly) { 609 final View v; 610 611 if (convertView == null) { 612 v = item.createView(mContext, mInflater, parent); 613 } else { 614 v = convertView; 615 } 616 item.bindView(v, measureOnly); 617 618 return v; 619 } 620 getLayoutInflater()621 public LayoutInflater getLayoutInflater() { 622 return mInflater; 623 } 624 getDateBuilder()625 public FormattedDateBuilder getDateBuilder() { 626 return mDateBuilder; 627 } 628 addItem(ConversationOverlayItem item)629 public int addItem(ConversationOverlayItem item) { 630 final int pos = mItems.size(); 631 item.setPosition(pos); 632 mItems.add(item); 633 return pos; 634 } 635 clear()636 public void clear() { 637 mItems.clear(); 638 notifyDataSetChanged(); 639 } 640 addConversationHeader(Conversation conv)641 public int addConversationHeader(Conversation conv) { 642 return addItem(new ConversationHeaderItem(conv)); 643 } 644 addConversationFooter(MessageHeaderItem headerItem)645 public int addConversationFooter(MessageHeaderItem headerItem) { 646 return addItem(new ConversationFooterItem(headerItem)); 647 } 648 addMessageHeader(ConversationMessage msg, boolean expanded, boolean showImages)649 public int addMessageHeader(ConversationMessage msg, boolean expanded, boolean showImages) { 650 return addItem(new MessageHeaderItem(this, mDateBuilder, msg, expanded, showImages)); 651 } 652 addMessageFooter(MessageHeaderItem headerItem)653 public int addMessageFooter(MessageHeaderItem headerItem) { 654 return addItem(new MessageFooterItem(this, headerItem)); 655 } 656 newMessageHeaderItem(ConversationViewAdapter adapter, FormattedDateBuilder dateBuilder, ConversationMessage message, boolean expanded, boolean showImages)657 public static MessageHeaderItem newMessageHeaderItem(ConversationViewAdapter adapter, 658 FormattedDateBuilder dateBuilder, ConversationMessage message, 659 boolean expanded, boolean showImages) { 660 return new MessageHeaderItem(adapter, dateBuilder, message, expanded, showImages); 661 } 662 newMessageFooterItem( ConversationViewAdapter adapter, MessageHeaderItem headerItem)663 public static MessageFooterItem newMessageFooterItem( 664 ConversationViewAdapter adapter, MessageHeaderItem headerItem) { 665 return new MessageFooterItem(adapter, headerItem); 666 } 667 addSuperCollapsedBlock(int start, int end, boolean hasDraft)668 public int addSuperCollapsedBlock(int start, int end, boolean hasDraft) { 669 return addItem(new SuperCollapsedBlockItem(start, end, hasDraft)); 670 } 671 replaceSuperCollapsedBlock(SuperCollapsedBlockItem blockToRemove, Collection<ConversationOverlayItem> replacements)672 public void replaceSuperCollapsedBlock(SuperCollapsedBlockItem blockToRemove, 673 Collection<ConversationOverlayItem> replacements) { 674 final int pos = mItems.indexOf(blockToRemove); 675 if (pos == -1) { 676 return; 677 } 678 679 mItems.remove(pos); 680 mItems.addAll(pos, replacements); 681 682 // update position for all items 683 for (int i = 0, size = mItems.size(); i < size; i++) { 684 mItems.get(i).setPosition(i); 685 } 686 } 687 updateItemsForMessage(ConversationMessage message, List<Integer> affectedPositions)688 public void updateItemsForMessage(ConversationMessage message, 689 List<Integer> affectedPositions) { 690 for (int i = 0, len = mItems.size(); i < len; i++) { 691 final ConversationOverlayItem item = mItems.get(i); 692 if (item.belongsToMessage(message)) { 693 item.setMessage(message); 694 affectedPositions.add(i); 695 } 696 } 697 } 698 699 /** 700 * Remove and return the {@link ConversationFooterItem} from the adapter. 701 */ removeFooterItem()702 public ConversationFooterItem removeFooterItem() { 703 final int count = mItems.size(); 704 if (count < 4) { 705 LogUtils.e(LOG_TAG, "not enough items in the adapter. count: %s", count); 706 return null; 707 } 708 final ConversationFooterItem item = (ConversationFooterItem) mItems.remove(count - 1); 709 if (item == null) { 710 LogUtils.e(LOG_TAG, "removed wrong overlay item: %s", item); 711 return null; 712 } 713 714 return item; 715 } 716 getFooterItem()717 public ConversationFooterItem getFooterItem() { 718 final int count = mItems.size(); 719 if (count < 4) { 720 LogUtils.e(LOG_TAG, "not enough items in the adapter. count: %s", count); 721 return null; 722 } 723 final ConversationOverlayItem item = mItems.get(count - 1); 724 try { 725 return (ConversationFooterItem) item; 726 } catch (ClassCastException e) { 727 LogUtils.e(LOG_TAG, "Last item is not a conversation footer. type: %s", item.getType()); 728 return null; 729 } 730 } 731 732 /** 733 * Returns true if the item before this one is of type 734 * {@link #VIEW_TYPE_SUPER_COLLAPSED_BLOCK}. 735 */ isPreviousItemSuperCollapsed(ConversationOverlayItem item)736 public boolean isPreviousItemSuperCollapsed(ConversationOverlayItem item) { 737 // super-collapsed will be the item just before the header 738 final int position = item.getPosition() - 1; 739 final int count = mItems.size(); 740 return !(position < 0 || position >= count) 741 && mItems.get(position).getType() == VIEW_TYPE_SUPER_COLLAPSED_BLOCK; 742 } 743 744 // This should be a safe call since all containers should have at least a conv header and a 745 // message header. focusFirstMessageHeader()746 public boolean focusFirstMessageHeader() { 747 if (mItems.size() > 1) { 748 final View v = mItems.get(1).getFocusableView(); 749 if (v != null && v.isShown() && v.isFocusable()) { 750 v.requestFocus(); 751 return true; 752 } 753 } 754 return false; 755 } 756 757 /** 758 * Find the next view that should grab focus with respect to the current position. 759 */ getNextOverlayView(View curr, boolean isDown, Set<View> scraps)760 public View getNextOverlayView(View curr, boolean isDown, Set<View> scraps) { 761 // First find the root view of the overlay item 762 while (curr.getTag() != OVERLAY_ITEM_ROOT_TAG) { 763 final ViewParent parent = curr.getParent(); 764 if (parent != null && parent instanceof View) { 765 curr = (View) parent; 766 } else { 767 return null; 768 } 769 } 770 771 // Find the position of the root view 772 for (int i = 0; i < mItems.size(); i++) { 773 if (mItems.get(i).mRootView == curr) { 774 // Found view, now find the next applicable view 775 if (isDown && i >= 0) { 776 while (++i < mItems.size()) { 777 final ConversationOverlayItem item = mItems.get(i); 778 final View next = item.getFocusableView(); 779 if (item.mRootView != null && !scraps.contains(item.mRootView) && 780 next != null && next.isFocusable()) { 781 return next; 782 } 783 } 784 } else { 785 while (--i >= 0) { 786 final ConversationOverlayItem item = mItems.get(i); 787 final View next = item.getFocusableView(); 788 if (item.mRootView != null && !scraps.contains(item.mRootView) && 789 next != null && next.isFocusable()) { 790 return next; 791 } 792 } 793 } 794 return null; 795 } 796 } 797 return null; 798 } 799 800 getBidiFormatter()801 public BidiFormatter getBidiFormatter() { 802 return mBidiFormatter; 803 } 804 getOnKeyListener()805 public View.OnKeyListener getOnKeyListener() { 806 return mOnKeyListener; 807 } 808 } 809