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