• 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 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 
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             // 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
bindView(View v, boolean measureOnly)151         public void bindView(View v, boolean measureOnly) {
152             ConversationViewHeader header = (ConversationViewHeader) v;
153             header.bind(this);
154             mRootView = v;
155         }
156 
157         @Override
isContiguous()158         public boolean isContiguous() {
159             return true;
160         }
161 
162         @Override
getOnKeyListener()163         public View.OnKeyListener getOnKeyListener() {
164             return mOnKeyListener;
165         }
166 
getAdapter()167         public ConversationViewAdapter getAdapter() {
168             return ConversationViewAdapter.this;
169         }
170     }
171 
172     public class ConversationFooterItem extends ConversationOverlayItem {
173         private MessageHeaderItem mLastMessageHeaderItem;
174 
ConversationFooterItem(MessageHeaderItem lastMessageHeaderItem)175         public ConversationFooterItem(MessageHeaderItem lastMessageHeaderItem) {
176             setLastMessageHeaderItem(lastMessageHeaderItem);
177         }
178 
179         @Override
getType()180         public @ConversationViewType int getType() {
181             return VIEW_TYPE_CONVERSATION_FOOTER;
182         }
183 
184         @Override
createView(Context context, LayoutInflater inflater, ViewGroup parent)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
bindView(View v, boolean measureOnly)200         public void bindView(View v, boolean measureOnly) {
201             ((ConversationFooterView) v).bind(this);
202             mRootView = v;
203         }
204 
205         @Override
rebindView(View view)206         public void rebindView(View view) {
207             ((ConversationFooterView) view).rebind(this);
208             mRootView = view;
209         }
210 
211         @Override
getFocusableView()212         public View getFocusableView() {
213             return mRootView.findViewById(R.id.reply_button);
214         }
215 
216         @Override
isContiguous()217         public boolean isContiguous() {
218             return true;
219         }
220 
221         @Override
getOnKeyListener()222         public View.OnKeyListener getOnKeyListener() {
223             return mOnKeyListener;
224         }
225 
getLastMessageHeaderItem()226         public MessageHeaderItem getLastMessageHeaderItem() {
227             return mLastMessageHeaderItem;
228         }
229 
setLastMessageHeaderItem(MessageHeaderItem lastMessageHeaderItem)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 
MessageHeaderItem(ConversationViewAdapter adapter, FormattedDateBuilder dateBuilder, ConversationMessage message, boolean expanded, boolean showImages)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 
getMessage()265         public ConversationMessage getMessage() {
266             return mMessage;
267         }
268 
269         @Override
getType()270         public @ConversationViewType int getType() {
271             return VIEW_TYPE_MESSAGE_HEADER;
272         }
273 
274         @Override
createView(Context context, LayoutInflater inflater, ViewGroup parent)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
bindView(View v, boolean measureOnly)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
getFocusableView()301         public View getFocusableView() {
302             return mRootView.findViewById(R.id.upper_header);
303         }
304 
305         @Override
onModelUpdated(View v)306         public void onModelUpdated(View v) {
307             final MessageHeaderView header = (MessageHeaderView) v;
308             header.refresh();
309         }
310 
311         @Override
isContiguous()312         public boolean isContiguous() {
313             return !isExpanded();
314         }
315 
316         @Override
getOnKeyListener()317         public View.OnKeyListener getOnKeyListener() {
318             return mAdapter.getOnKeyListener();
319         }
320 
321         @Override
isExpanded()322         public boolean isExpanded() {
323             return mExpanded;
324         }
325 
setExpanded(boolean expanded)326         public void setExpanded(boolean expanded) {
327             if (mExpanded != expanded) {
328                 mExpanded = expanded;
329             }
330         }
331 
getShowImages()332         public boolean getShowImages() {
333             return mShowImages;
334         }
335 
setShowImages(boolean showImages)336         public void setShowImages(boolean showImages) {
337             mShowImages = showImages;
338         }
339 
340         @Override
canBecomeSnapHeader()341         public boolean canBecomeSnapHeader() {
342             return isExpanded();
343         }
344 
345         @Override
canPushSnapHeader()346         public boolean canPushSnapHeader() {
347             return true;
348         }
349 
350         @Override
belongsToMessage(ConversationMessage message)351         public boolean belongsToMessage(ConversationMessage message) {
352             return Objects.equal(mMessage, message);
353         }
354 
355         @Override
setMessage(ConversationMessage message)356         public void setMessage(ConversationMessage message) {
357             mMessage = message;
358         }
359 
getTimestampShort()360         public CharSequence getTimestampShort() {
361             ensureTimestamps();
362             return mTimestampShort;
363         }
364 
getTimestampLong()365         public CharSequence getTimestampLong() {
366             ensureTimestamps();
367             return mTimestampLong;
368         }
369 
getTimestampFull()370         public CharSequence getTimestampFull() {
371             ensureTimestamps();
372             return mTimestampFull;
373         }
374 
ensureTimestamps()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 
getAdapter()384         public ConversationViewAdapter getAdapter() {
385             return mAdapter;
386         }
387 
388         @Override
rebindView(View view)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 
MessageFooterItem(ConversationViewAdapter adapter, MessageHeaderItem item)405         private MessageFooterItem(ConversationViewAdapter adapter, MessageHeaderItem item) {
406             mAdapter = adapter;
407             mHeaderItem = item;
408         }
409 
410         @Override
getType()411         public @ConversationViewType int getType() {
412             return VIEW_TYPE_MESSAGE_FOOTER;
413         }
414 
415         @Override
createView(Context context, LayoutInflater inflater, ViewGroup parent)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
bindView(View v, boolean measureOnly)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
isContiguous()436         public boolean isContiguous() {
437             return true;
438         }
439 
440         @Override
getOnKeyListener()441         public View.OnKeyListener getOnKeyListener() {
442             return mAdapter.getOnKeyListener();
443         }
444 
445         @Override
isExpanded()446         public boolean isExpanded() {
447             return mHeaderItem.isExpanded();
448         }
449 
450         @Override
getGravity()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
getHeight()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 
getHeaderItem()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 
SuperCollapsedBlockItem(int start, int end, boolean hasDraft)478         private SuperCollapsedBlockItem(int start, int end, boolean hasDraft) {
479             mStart = start;
480             mEnd = end;
481             mHasDraft = hasDraft;
482         }
483 
484         @Override
getType()485         public @ConversationViewType int getType() {
486             return VIEW_TYPE_SUPER_COLLAPSED_BLOCK;
487         }
488 
489         @Override
createView(Context context, LayoutInflater inflater, ViewGroup parent)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
bindView(View v, boolean measureOnly)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
isContiguous()510         public boolean isContiguous() {
511             return true;
512         }
513 
514         @Override
getOnKeyListener()515         public View.OnKeyListener getOnKeyListener() {
516             return mOnKeyListener;
517         }
518 
519         @Override
isExpanded()520         public boolean isExpanded() {
521             return false;
522         }
523 
getStart()524         public int getStart() {
525             return mStart;
526         }
527 
getEnd()528         public int getEnd() {
529             return mEnd;
530         }
531 
hasDraft()532         public boolean hasDraft() {
533             return mHasDraft;
534         }
535 
536         @Override
canPushSnapHeader()537         public boolean canPushSnapHeader() {
538             return true;
539         }
540     }
541 
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)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
getCount()579     public int getCount() {
580         return mItems.size();
581     }
582 
583     @Override
getItemViewType(int position)584     public @ConversationViewType int getItemViewType(int position) {
585         return mItems.get(position).getType();
586     }
587 
588     @Override
getViewTypeCount()589     public int getViewTypeCount() {
590         return VIEW_TYPE_COUNT;
591     }
592 
593     @Override
getItem(int position)594     public ConversationOverlayItem getItem(int position) {
595         return mItems.get(position);
596     }
597 
598     @Override
getItemId(int position)599     public long getItemId(int position) {
600         return position; // TODO: ensure this works well enough
601     }
602 
603     @Override
getView(int position, View convertView, ViewGroup parent)604     public View getView(int position, View convertView, ViewGroup parent) {
605         return getView(getItem(position), convertView, parent, false /* measureOnly */);
606     }
607 
getView(ConversationOverlayItem item, View convertView, ViewGroup parent, boolean measureOnly)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 
getLayoutInflater()622     public LayoutInflater getLayoutInflater() {
623         return mInflater;
624     }
625 
getDateBuilder()626     public FormattedDateBuilder getDateBuilder() {
627         return mDateBuilder;
628     }
629 
addItem(ConversationOverlayItem item)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 
clear()637     public void clear() {
638         mItems.clear();
639         notifyDataSetChanged();
640     }
641 
addConversationHeader(Conversation conv)642     public int addConversationHeader(Conversation conv) {
643         return addItem(new ConversationHeaderItem(conv));
644     }
645 
addConversationFooter(MessageHeaderItem headerItem)646     public int addConversationFooter(MessageHeaderItem headerItem) {
647         return addItem(new ConversationFooterItem(headerItem));
648     }
649 
addMessageHeader(ConversationMessage msg, boolean expanded, boolean showImages)650     public int addMessageHeader(ConversationMessage msg, boolean expanded, boolean showImages) {
651         return addItem(new MessageHeaderItem(this, mDateBuilder, msg, expanded, showImages));
652     }
653 
addMessageFooter(MessageHeaderItem headerItem)654     public int addMessageFooter(MessageHeaderItem headerItem) {
655         return addItem(new MessageFooterItem(this, headerItem));
656     }
657 
newMessageHeaderItem(ConversationViewAdapter adapter, FormattedDateBuilder dateBuilder, ConversationMessage message, boolean expanded, boolean showImages)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 
newMessageFooterItem( ConversationViewAdapter adapter, MessageHeaderItem headerItem)664     public static MessageFooterItem newMessageFooterItem(
665             ConversationViewAdapter adapter, MessageHeaderItem headerItem) {
666         return new MessageFooterItem(adapter, headerItem);
667     }
668 
addSuperCollapsedBlock(int start, int end, boolean hasDraft)669     public int addSuperCollapsedBlock(int start, int end, boolean hasDraft) {
670         return addItem(new SuperCollapsedBlockItem(start, end, hasDraft));
671     }
672 
replaceSuperCollapsedBlock(SuperCollapsedBlockItem blockToRemove, Collection<ConversationOverlayItem> replacements)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 
updateItemsForMessage(ConversationMessage message, List<Integer> affectedPositions)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      */
removeFooterItem()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 
getFooterItem()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      */
isPreviousItemSuperCollapsed(ConversationOverlayItem item)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?
focusFirstMessageHeader()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      */
getViewPosition(View v)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      */
getNextOverlayView(int position, boolean isDown)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 
shouldInterceptLeftRightEvents(@dRes int id, boolean isLeft, boolean isRight, boolean twoPaneLand)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.
shouldNavigateAway(@dRes int id, boolean isLeft, boolean twoPaneLand)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 
getBidiFormatter()823     public BidiFormatter getBidiFormatter() {
824         return mBidiFormatter;
825     }
826 
getOnKeyListener()827     public View.OnKeyListener getOnKeyListener() {
828         return mOnKeyListener;
829     }
830 }
831