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