ConversationViewAdapter.java revision cee3c90574b48ccaa0f8b9f9341383c231ed41d2
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.view.Gravity;
24import android.view.LayoutInflater;
25import android.view.View;
26import android.view.ViewGroup;
27import android.widget.BaseAdapter;
28
29import com.android.mail.ContactInfoSource;
30import com.android.mail.FormattedDateBuilder;
31import com.android.mail.R;
32import com.android.mail.browse.ConversationViewHeader.ConversationViewHeaderCallbacks;
33import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks;
34import com.android.mail.browse.SuperCollapsedBlock.OnClickListener;
35import com.android.mail.providers.Address;
36import com.android.mail.providers.Conversation;
37import com.android.mail.providers.UIProvider;
38import com.android.mail.ui.ControllableActivity;
39import com.android.mail.utils.VeiledAddressMatcher;
40import com.google.common.base.Objects;
41import com.google.common.collect.Lists;
42
43import java.util.Collection;
44import java.util.List;
45import java.util.Map;
46
47/**
48 * A specialized adapter that contains overlay views to draw on top of the underlying conversation
49 * WebView. Each independently drawn overlay view gets its own item in this adapter, and indices
50 * in this adapter do not necessarily line up with cursor indices. For example, an expanded
51 * message may have a header and footer, and since they are not drawn coupled together, they each
52 * get an adapter item.
53 * <p>
54 * Each item in this adapter is a {@link ConversationOverlayItem} to expose enough information
55 * to {@link ConversationContainer} so that it can position overlays properly.
56 *
57 */
58public class ConversationViewAdapter extends BaseAdapter {
59
60    private Context mContext;
61    private final FormattedDateBuilder mDateBuilder;
62    private final ConversationAccountController mAccountController;
63    private final LoaderManager mLoaderManager;
64    private final FragmentManager mFragmentManager;
65    private final MessageHeaderViewCallbacks mMessageCallbacks;
66    private final ContactInfoSource mContactInfoSource;
67    private ConversationViewHeaderCallbacks mConversationCallbacks;
68    private OnClickListener mSuperCollapsedListener;
69    private Map<String, Address> mAddressCache;
70    private final LayoutInflater mInflater;
71
72    private final List<ConversationOverlayItem> mItems;
73    private final VeiledAddressMatcher mMatcher;
74
75    public static final int VIEW_TYPE_CONVERSATION_HEADER = 0;
76    public static final int VIEW_TYPE_AD_HEADER = 1;
77    public static final int VIEW_TYPE_MESSAGE_HEADER = 2;
78    public static final int VIEW_TYPE_MESSAGE_FOOTER = 3;
79    public static final int VIEW_TYPE_SUPER_COLLAPSED_BLOCK = 4;
80    public static final int VIEW_TYPE_BORDER = 5;
81    public static final int VIEW_TYPE_COUNT = 6;
82
83    public class ConversationHeaderItem extends ConversationOverlayItem {
84        public final Conversation mConversation;
85
86        private ConversationHeaderItem(Conversation conv) {
87            mConversation = conv;
88        }
89
90        @Override
91        public int getType() {
92            return VIEW_TYPE_CONVERSATION_HEADER;
93        }
94
95        @Override
96        public View createView(Context context, LayoutInflater inflater, ViewGroup parent) {
97            final ConversationViewHeader headerView = (ConversationViewHeader) inflater.inflate(
98                    R.layout.conversation_view_header, parent, false);
99            headerView.setCallbacks(mConversationCallbacks, mAccountController);
100            headerView.bind(this);
101            headerView.setSubject(mConversation.subject);
102            if (mAccountController.getAccount().supportsCapability(
103                    UIProvider.AccountCapabilities.MULTIPLE_FOLDERS_PER_CONV)) {
104                headerView.setFolders(mConversation);
105            }
106
107            return headerView;
108        }
109
110        @Override
111        public void bindView(View v, boolean measureOnly) {
112            ConversationViewHeader header = (ConversationViewHeader) v;
113            header.bind(this);
114        }
115
116        @Override
117        public boolean isContiguous() {
118            return true;
119        }
120
121    }
122
123    public static class MessageHeaderItem extends ConversationOverlayItem {
124
125        private final ConversationViewAdapter mAdapter;
126
127        private ConversationMessage mMessage;
128
129        // view state variables
130        private boolean mExpanded;
131        public boolean detailsExpanded;
132        private boolean mShowImages;
133
134        // cached values to speed up re-rendering during view recycling
135        private CharSequence mTimestampShort;
136        private CharSequence mTimestampLong;
137        private long mTimestampMs;
138        private FormattedDateBuilder mDateBuilder;
139        public CharSequence recipientSummaryText;
140
141        MessageHeaderItem(ConversationViewAdapter adapter, FormattedDateBuilder dateBuilder,
142                ConversationMessage message, boolean expanded, boolean showImages) {
143            mAdapter = adapter;
144            mDateBuilder = dateBuilder;
145            mMessage = message;
146            mExpanded = expanded;
147            mShowImages = showImages;
148
149            detailsExpanded = false;
150        }
151
152        public ConversationMessage getMessage() {
153            return mMessage;
154        }
155
156        @Override
157        public int getType() {
158            return VIEW_TYPE_MESSAGE_HEADER;
159        }
160
161        @Override
162        public View createView(Context context, LayoutInflater inflater, ViewGroup parent) {
163            final MessageHeaderView v = (MessageHeaderView) inflater.inflate(
164                    R.layout.conversation_message_header, parent, false);
165            v.initialize(mAdapter.mAccountController,
166                    mAdapter.mAddressCache);
167            v.setCallbacks(mAdapter.mMessageCallbacks);
168            v.setContactInfoSource(mAdapter.mContactInfoSource);
169            v.setVeiledMatcher(mAdapter.mMatcher);
170            return v;
171        }
172
173        @Override
174        public void bindView(View v, boolean measureOnly) {
175            final MessageHeaderView header = (MessageHeaderView) v;
176            header.bind(this, measureOnly);
177        }
178
179        @Override
180        public void onModelUpdated(View v) {
181            final MessageHeaderView header = (MessageHeaderView) v;
182            header.refresh();
183        }
184
185        @Override
186        public boolean isContiguous() {
187            return !isExpanded();
188        }
189
190        @Override
191        public boolean isExpanded() {
192            return mExpanded;
193        }
194
195        public void setExpanded(boolean expanded) {
196            if (mExpanded != expanded) {
197                mExpanded = expanded;
198            }
199        }
200
201        public boolean getShowImages() {
202            return mShowImages;
203        }
204
205        public void setShowImages(boolean showImages) {
206            mShowImages = showImages;
207        }
208
209        @Override
210        public boolean canBecomeSnapHeader() {
211            return isExpanded();
212        }
213
214        @Override
215        public boolean canPushSnapHeader() {
216            return true;
217        }
218
219        @Override
220        public boolean belongsToMessage(ConversationMessage message) {
221            return Objects.equal(mMessage, message);
222        }
223
224        @Override
225        public void setMessage(ConversationMessage message) {
226            mMessage = message;
227        }
228
229        public CharSequence getTimestampShort() {
230            ensureTimestamps();
231            return mTimestampShort;
232        }
233
234        public CharSequence getTimestampLong() {
235            ensureTimestamps();
236            return mTimestampLong;
237        }
238
239        private void ensureTimestamps() {
240            if (mMessage.dateReceivedMs != mTimestampMs) {
241                mTimestampMs = mMessage.dateReceivedMs;
242                mTimestampShort = mDateBuilder.formatShortDate(mTimestampMs);
243                mTimestampLong = mDateBuilder.formatLongDateTime(mTimestampMs);
244            }
245        }
246
247        public ConversationViewAdapter getAdapter() {
248            return mAdapter;
249        }
250    }
251
252    public class MessageFooterItem extends ConversationOverlayItem {
253        /**
254         * A footer can only exist if there is a matching header. Requiring a header allows a
255         * footer to stay in sync with the expanded state of the header.
256         */
257        private final MessageHeaderItem mHeaderitem;
258
259        private MessageFooterItem(MessageHeaderItem item) {
260            mHeaderitem = item;
261        }
262
263        @Override
264        public int getType() {
265            return VIEW_TYPE_MESSAGE_FOOTER;
266        }
267
268        @Override
269        public View createView(Context context, LayoutInflater inflater, ViewGroup parent) {
270            final MessageFooterView v = (MessageFooterView) inflater.inflate(
271                    R.layout.conversation_message_footer, parent, false);
272            v.initialize(mLoaderManager, mFragmentManager);
273            return v;
274        }
275
276        @Override
277        public void bindView(View v, boolean measureOnly) {
278            final MessageFooterView attachmentsView = (MessageFooterView) v;
279            attachmentsView.bind(mHeaderitem, mAccountController.getAccount().uri, measureOnly);
280        }
281
282        @Override
283        public boolean isContiguous() {
284            return true;
285        }
286
287        @Override
288        public boolean isExpanded() {
289            return mHeaderitem.isExpanded();
290        }
291
292        @Override
293        public int getGravity() {
294            // attachments are top-aligned within their spacer area
295            // Attachments should stay near the body they belong to, even when zoomed far in.
296            return Gravity.TOP;
297        }
298
299        @Override
300        public int getHeight() {
301            // a footer may change height while its view does not exist because it is offscreen
302            // (but the header is onscreen and thus collapsible)
303            if (!mHeaderitem.isExpanded()) {
304                return 0;
305            }
306            return super.getHeight();
307        }
308    }
309
310    public class SuperCollapsedBlockItem extends ConversationOverlayItem {
311
312        private final int mStart;
313        private int mEnd;
314
315        private SuperCollapsedBlockItem(int start, int end) {
316            mStart = start;
317            mEnd = end;
318        }
319
320        @Override
321        public int getType() {
322            return VIEW_TYPE_SUPER_COLLAPSED_BLOCK;
323        }
324
325        @Override
326        public View createView(Context context, LayoutInflater inflater, ViewGroup parent) {
327            final SuperCollapsedBlock scb = (SuperCollapsedBlock) inflater.inflate(
328                    R.layout.super_collapsed_block, parent, false);
329            scb.initialize(mSuperCollapsedListener);
330            return scb;
331        }
332
333        @Override
334        public void bindView(View v, boolean measureOnly) {
335            final SuperCollapsedBlock scb = (SuperCollapsedBlock) v;
336            scb.bind(this);
337        }
338
339        @Override
340        public boolean isContiguous() {
341            return true;
342        }
343
344        @Override
345        public boolean isExpanded() {
346            return false;
347        }
348
349        public int getStart() {
350            return mStart;
351        }
352
353        public int getEnd() {
354            return mEnd;
355        }
356
357        @Override
358        public boolean canPushSnapHeader() {
359            return true;
360        }
361    }
362
363
364    public class BorderItem extends ConversationOverlayItem {
365        private final boolean mContiguous;
366        private boolean mExpanded;
367        private final boolean mFirstBorder;
368        private boolean mLastBorder;
369
370        public BorderItem(boolean contiguous, boolean isExpanded,
371                boolean firstBorder, boolean lastBorder) {
372            mContiguous = contiguous;
373            mExpanded = isExpanded;
374            mFirstBorder = firstBorder;
375            mLastBorder = lastBorder;
376        }
377
378        @Override
379        public int getType() {
380            return VIEW_TYPE_BORDER;
381        }
382
383        @Override
384        public View createView(Context context, LayoutInflater inflater, ViewGroup parent) {
385            return inflater.inflate(R.layout.card_border, parent, false);
386        }
387
388        @Override
389        public void bindView(View v, boolean measureOnly) {
390            final BorderView border = (BorderView) v;
391            border.bind(this, measureOnly);
392        }
393
394        @Override
395        public boolean isContiguous() {
396            return mContiguous;
397        }
398
399        @Override
400        public boolean isExpanded() {
401            return mExpanded;
402        }
403
404        public void setExpanded(boolean isExpanded) {
405            mExpanded = isExpanded;
406        }
407
408        @Override
409        public boolean canPushSnapHeader() {
410            return false;
411        }
412
413        public boolean isFirstBorder() {
414            return mFirstBorder;
415        }
416
417        public boolean isLastBorder() {
418            return mLastBorder;
419        }
420
421        public void setIsLastBorder(boolean isLastBorder) {
422            mLastBorder = isLastBorder;
423        }
424
425        public ConversationViewAdapter getAdapter() {
426            return ConversationViewAdapter.this;
427        }
428
429        @Override
430        public void rebindView(View view) {
431            bindView(view, false);
432        }
433    }
434
435    public ConversationViewAdapter(ControllableActivity controllableActivity,
436            ConversationAccountController accountController,
437            LoaderManager loaderManager,
438            MessageHeaderViewCallbacks messageCallbacks,
439            ContactInfoSource contactInfoSource,
440            ConversationViewHeaderCallbacks convCallbacks,
441            SuperCollapsedBlock.OnClickListener scbListener, Map<String, Address> addressCache,
442            FormattedDateBuilder dateBuilder) {
443        mContext = controllableActivity.getActivityContext();
444        mDateBuilder = dateBuilder;
445        mAccountController = accountController;
446        mLoaderManager = loaderManager;
447        mFragmentManager = controllableActivity.getFragmentManager();
448        mMessageCallbacks = messageCallbacks;
449        mContactInfoSource = contactInfoSource;
450        mConversationCallbacks = convCallbacks;
451        mSuperCollapsedListener = scbListener;
452        mAddressCache = addressCache;
453        mInflater = LayoutInflater.from(mContext);
454
455        mItems = Lists.newArrayList();
456        mMatcher = controllableActivity.getAccountController().getVeiledAddressMatcher();
457    }
458
459    @Override
460    public int getCount() {
461        return mItems.size();
462    }
463
464    @Override
465    public int getItemViewType(int position) {
466        return mItems.get(position).getType();
467    }
468
469    @Override
470    public int getViewTypeCount() {
471        return VIEW_TYPE_COUNT;
472    }
473
474    @Override
475    public ConversationOverlayItem getItem(int position) {
476        return mItems.get(position);
477    }
478
479    @Override
480    public long getItemId(int position) {
481        return position; // TODO: ensure this works well enough
482    }
483
484    @Override
485    public View getView(int position, View convertView, ViewGroup parent) {
486        return getView(getItem(position), convertView, parent, false /* measureOnly */);
487    }
488
489    public View getView(ConversationOverlayItem item, View convertView, ViewGroup parent,
490            boolean measureOnly) {
491        final View v;
492
493        if (convertView == null) {
494            v = item.createView(mContext, mInflater, parent);
495        } else {
496            v = convertView;
497        }
498        item.bindView(v, measureOnly);
499
500        return v;
501    }
502
503    public FormattedDateBuilder getDateBuilder() {
504        return mDateBuilder;
505    }
506
507    public int addItem(ConversationOverlayItem item) {
508        final int pos = mItems.size();
509        item.setPosition(pos);
510        mItems.add(item);
511        return pos;
512    }
513
514    public void clear() {
515        mItems.clear();
516        notifyDataSetChanged();
517    }
518
519    public int addConversationHeader(Conversation conv) {
520        return addItem(new ConversationHeaderItem(conv));
521    }
522
523    public int addMessageHeader(ConversationMessage msg, boolean expanded, boolean showImages) {
524        return addItem(new MessageHeaderItem(this, mDateBuilder, msg, expanded, showImages));
525    }
526
527    public int addMessageFooter(MessageHeaderItem headerItem) {
528        return addItem(new MessageFooterItem(headerItem));
529    }
530
531    public static MessageHeaderItem newMessageHeaderItem(ConversationViewAdapter adapter,
532            FormattedDateBuilder dateBuilder, ConversationMessage message,
533            boolean expanded, boolean showImages) {
534        return new MessageHeaderItem(adapter, dateBuilder, message, expanded, showImages);
535    }
536
537    public MessageFooterItem newMessageFooterItem(MessageHeaderItem headerItem) {
538        return new MessageFooterItem(headerItem);
539    }
540
541    public int addSuperCollapsedBlock(int start, int end) {
542        return addItem(new SuperCollapsedBlockItem(start, end));
543    }
544
545    public int addBorder(
546            boolean contiguous, boolean expanded, boolean firstBorder, boolean lastBorder) {
547        return addItem(new BorderItem(contiguous, expanded, firstBorder, lastBorder));
548    }
549
550    public BorderItem newBorderItem(boolean contiguous, boolean expanded) {
551        return new BorderItem(
552                contiguous, expanded, false /* firstBorder */, false /* lastBorder */);
553    }
554
555    public void replaceSuperCollapsedBlock(SuperCollapsedBlockItem blockToRemove,
556            Collection<ConversationOverlayItem> replacements) {
557        final int pos = mItems.indexOf(blockToRemove);
558        if (pos == -1) {
559            return;
560        }
561
562        mItems.remove(pos);
563        mItems.addAll(pos, replacements);
564
565        // update position for all items
566        for (int i = 0, size = mItems.size(); i < size; i++) {
567            mItems.get(i).setPosition(i);
568        }
569    }
570
571    public void updateItemsForMessage(ConversationMessage message,
572            List<Integer> affectedPositions) {
573        for (int i = 0, len = mItems.size(); i < len; i++) {
574            final ConversationOverlayItem item = mItems.get(i);
575            if (item.belongsToMessage(message)) {
576                item.setMessage(message);
577                affectedPositions.add(i);
578            }
579        }
580    }
581}
582