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
18package com.android.mail.browse;
19
20import android.app.FragmentManager;
21import android.app.LoaderManager;
22import android.content.Context;
23import android.support.annotation.IntDef;
24import android.support.v4.text.BidiFormatter;
25import android.view.Gravity;
26import android.view.LayoutInflater;
27import android.view.View;
28import android.view.ViewGroup;
29import android.view.ViewParent;
30import android.widget.BaseAdapter;
31
32import com.android.emailcommon.mail.Address;
33import com.android.mail.ContactInfoSource;
34import com.android.mail.FormattedDateBuilder;
35import com.android.mail.R;
36import com.android.mail.browse.ConversationFooterView.ConversationFooterCallbacks;
37import com.android.mail.browse.ConversationViewHeader.ConversationViewHeaderCallbacks;
38import com.android.mail.browse.MessageFooterView.MessageFooterCallbacks;
39import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks;
40import com.android.mail.browse.SuperCollapsedBlock.OnClickListener;
41import com.android.mail.providers.Conversation;
42import com.android.mail.providers.UIProvider;
43import com.android.mail.ui.ControllableActivity;
44import com.android.mail.ui.ConversationUpdater;
45import com.android.mail.utils.LogTag;
46import com.android.mail.utils.LogUtils;
47import com.android.mail.utils.VeiledAddressMatcher;
48import com.google.common.base.Objects;
49import com.google.common.collect.Lists;
50
51import java.lang.annotation.Retention;
52import java.lang.annotation.RetentionPolicy;
53import java.util.Collection;
54import java.util.List;
55import java.util.Map;
56import 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 */
69public 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