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