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.IntDef;
     24 import android.support.v4.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 
    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             return v;
    145         }
    146 
    147         @Override
    148         public void bindView(View v, boolean measureOnly) {
    149             ConversationViewHeader header = (ConversationViewHeader) v;
    150             header.bind(this);
    151         }
    152 
    153         @Override
    154         public boolean isContiguous() {
    155             return true;
    156         }
    157 
    158         @Override
    159         public View.OnKeyListener getOnKeyListener() {
    160             return mOnKeyListener;
    161         }
    162 
    163         public ConversationViewAdapter getAdapter() {
    164             return ConversationViewAdapter.this;
    165         }
    166     }
    167 
    168     public class ConversationFooterItem extends ConversationOverlayItem {
    169         private MessageHeaderItem mLastMessageHeaderItem;
    170 
    171         public ConversationFooterItem(MessageHeaderItem lastMessageHeaderItem) {
    172             setLastMessageHeaderItem(lastMessageHeaderItem);
    173         }
    174 
    175         @Override
    176         public @ConversationViewType int getType() {
    177             return VIEW_TYPE_CONVERSATION_FOOTER;
    178         }
    179 
    180         @Override
    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
    196         public void bindView(View v, boolean measureOnly) {
    197             ((ConversationFooterView) v).bind(this);
    198             mRootView = v;
    199         }
    200 
    201         @Override
    202         public void rebindView(View view) {
    203             ((ConversationFooterView) view).rebind(this);
    204             mRootView = view;
    205         }
    206 
    207         @Override
    208         public View getFocusableView() {
    209             return mRootView.findViewById(R.id.reply_button);
    210         }
    211 
    212         @Override
    213         public boolean isContiguous() {
    214             return true;
    215         }
    216 
    217         @Override
    218         public View.OnKeyListener getOnKeyListener() {
    219             return mOnKeyListener;
    220         }
    221 
    222         public MessageHeaderItem getLastMessageHeaderItem() {
    223             return mLastMessageHeaderItem;
    224         }
    225 
    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 
    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 
    261         public ConversationMessage getMessage() {
    262             return mMessage;
    263         }
    264 
    265         @Override
    266         public @ConversationViewType int getType() {
    267             return VIEW_TYPE_MESSAGE_HEADER;
    268         }
    269 
    270         @Override
    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
    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
    297         public View getFocusableView() {
    298             return mRootView.findViewById(R.id.upper_header);
    299         }
    300 
    301         @Override
    302         public void onModelUpdated(View v) {
    303             final MessageHeaderView header = (MessageHeaderView) v;
    304             header.refresh();
    305         }
    306 
    307         @Override
    308         public boolean isContiguous() {
    309             return !isExpanded();
    310         }
    311 
    312         @Override
    313         public View.OnKeyListener getOnKeyListener() {
    314             return mAdapter.getOnKeyListener();
    315         }
    316 
    317         @Override
    318         public boolean isExpanded() {
    319             return mExpanded;
    320         }
    321 
    322         public void setExpanded(boolean expanded) {
    323             if (mExpanded != expanded) {
    324                 mExpanded = expanded;
    325             }
    326         }
    327 
    328         public boolean getShowImages() {
    329             return mShowImages;
    330         }
    331 
    332         public void setShowImages(boolean showImages) {
    333             mShowImages = showImages;
    334         }
    335 
    336         @Override
    337         public boolean canBecomeSnapHeader() {
    338             return isExpanded();
    339         }
    340 
    341         @Override
    342         public boolean canPushSnapHeader() {
    343             return true;
    344         }
    345 
    346         @Override
    347         public boolean belongsToMessage(ConversationMessage message) {
    348             return Objects.equal(mMessage, message);
    349         }
    350 
    351         @Override
    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 
    359         public CharSequence getTimestampShort() {
    360             ensureTimestamps();
    361             return mTimestampShort;
    362         }
    363 
    364         public CharSequence getTimestampLong() {
    365             ensureTimestamps();
    366             return mTimestampLong;
    367         }
    368 
    369         public CharSequence getTimestampFull() {
    370             ensureTimestamps();
    371             return mTimestampFull;
    372         }
    373 
    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 
    383         public ConversationViewAdapter getAdapter() {
    384             return mAdapter;
    385         }
    386 
    387         @Override
    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 
    404         private MessageFooterItem(ConversationViewAdapter adapter, MessageHeaderItem item) {
    405             mAdapter = adapter;
    406             mHeaderItem = item;
    407         }
    408 
    409         @Override
    410         public @ConversationViewType int getType() {
    411             return VIEW_TYPE_MESSAGE_FOOTER;
    412         }
    413 
    414         @Override
    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
    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
    435         public boolean isContiguous() {
    436             return true;
    437         }
    438 
    439         @Override
    440         public View.OnKeyListener getOnKeyListener() {
    441             return mAdapter.getOnKeyListener();
    442         }
    443 
    444         @Override
    445         public boolean isExpanded() {
    446             return mHeaderItem.isExpanded();
    447         }
    448 
    449         @Override
    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
    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 
    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 
    477         private SuperCollapsedBlockItem(int start, int end, boolean hasDraft) {
    478             mStart = start;
    479             mEnd = end;
    480             mHasDraft = hasDraft;
    481         }
    482 
    483         @Override
    484         public @ConversationViewType int getType() {
    485             return VIEW_TYPE_SUPER_COLLAPSED_BLOCK;
    486         }
    487 
    488         @Override
    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
    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
    509         public boolean isContiguous() {
    510             return true;
    511         }
    512 
    513         @Override
    514         public View.OnKeyListener getOnKeyListener() {
    515             return mOnKeyListener;
    516         }
    517 
    518         @Override
    519         public boolean isExpanded() {
    520             return false;
    521         }
    522 
    523         public int getStart() {
    524             return mStart;
    525         }
    526 
    527         public int getEnd() {
    528             return mEnd;
    529         }
    530 
    531         public boolean hasDraft() {
    532             return mHasDraft;
    533         }
    534 
    535         @Override
    536         public boolean canPushSnapHeader() {
    537             return true;
    538         }
    539     }
    540 
    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
    578     public int getCount() {
    579         return mItems.size();
    580     }
    581 
    582     @Override
    583     public @ConversationViewType int getItemViewType(int position) {
    584         return mItems.get(position).getType();
    585     }
    586 
    587     @Override
    588     public int getViewTypeCount() {
    589         return VIEW_TYPE_COUNT;
    590     }
    591 
    592     @Override
    593     public ConversationOverlayItem getItem(int position) {
    594         return mItems.get(position);
    595     }
    596 
    597     @Override
    598     public long getItemId(int position) {
    599         return position; // TODO: ensure this works well enough
    600     }
    601 
    602     @Override
    603     public View getView(int position, View convertView, ViewGroup parent) {
    604         return getView(getItem(position), convertView, parent, false /* measureOnly */);
    605     }
    606 
    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 
    621     public LayoutInflater getLayoutInflater() {
    622         return mInflater;
    623     }
    624 
    625     public FormattedDateBuilder getDateBuilder() {
    626         return mDateBuilder;
    627     }
    628 
    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 
    636     public void clear() {
    637         mItems.clear();
    638         notifyDataSetChanged();
    639     }
    640 
    641     public int addConversationHeader(Conversation conv) {
    642         return addItem(new ConversationHeaderItem(conv));
    643     }
    644 
    645     public int addConversationFooter(MessageHeaderItem headerItem) {
    646         return addItem(new ConversationFooterItem(headerItem));
    647     }
    648 
    649     public int addMessageHeader(ConversationMessage msg, boolean expanded, boolean showImages) {
    650         return addItem(new MessageHeaderItem(this, mDateBuilder, msg, expanded, showImages));
    651     }
    652 
    653     public int addMessageFooter(MessageHeaderItem headerItem) {
    654         return addItem(new MessageFooterItem(this, headerItem));
    655     }
    656 
    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 
    663     public static MessageFooterItem newMessageFooterItem(
    664             ConversationViewAdapter adapter, MessageHeaderItem headerItem) {
    665         return new MessageFooterItem(adapter, headerItem);
    666     }
    667 
    668     public int addSuperCollapsedBlock(int start, int end, boolean hasDraft) {
    669         return addItem(new SuperCollapsedBlockItem(start, end, hasDraft));
    670     }
    671 
    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 
    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      */
    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 
    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      */
    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.
    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      */
    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 
    801     public BidiFormatter getBidiFormatter() {
    802         return mBidiFormatter;
    803     }
    804 
    805     public View.OnKeyListener getOnKeyListener() {
    806         return mOnKeyListener;
    807     }
    808 }
    809