Home | History | Annotate | Download | only in browse
      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