ConversationViewAdapter.java revision 69a6cdff8afde77ec9bcd75a5651ee212344019e
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.MessageCursor.ConversationMessage;
34import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks;
35import com.android.mail.browse.SuperCollapsedBlock.OnClickListener;
36import com.android.mail.providers.Address;
37import com.android.mail.providers.Conversation;
38import com.android.mail.providers.UIProvider;
39import com.android.mail.ui.ControllableActivity;
40import com.android.mail.utils.VeiledAddressMatcher;
41
42import com.google.common.base.Objects;
43import com.google.common.collect.Lists;
44
45import java.util.Collection;
46import java.util.List;
47import java.util.Map;
48
49/**
50 * A specialized adapter that contains overlay views to draw on top of the underlying conversation
51 * WebView. Each independently drawn overlay view gets its own item in this adapter, and indices
52 * in this adapter do not necessarily line up with cursor indices. For example, an expanded
53 * message may have a header and footer, and since they are not drawn coupled together, they each
54 * get an adapter item.
55 * <p>
56 * Each item in this adapter is a {@link ConversationOverlayItem} to expose enough information
57 * to {@link ConversationContainer} so that it can position overlays properly.
58 *
59 */
60public class ConversationViewAdapter extends BaseAdapter {
61
62    private Context mContext;
63    private final FormattedDateBuilder mDateBuilder;
64    private final ConversationAccountController mAccountController;
65    private final LoaderManager mLoaderManager;
66    private final FragmentManager mFragmentManager;
67    private final MessageHeaderViewCallbacks mMessageCallbacks;
68    private final ContactInfoSource mContactInfoSource;
69    private ConversationViewHeaderCallbacks mConversationCallbacks;
70    private OnClickListener mSuperCollapsedListener;
71    private Map<String, Address> mAddressCache;
72    private final LayoutInflater mInflater;
73
74    private final List<ConversationOverlayItem> mItems;
75    private final VeiledAddressMatcher mMatcher;
76
77    public static final int VIEW_TYPE_CONVERSATION_HEADER = 0;
78    public static final int VIEW_TYPE_MESSAGE_HEADER = 1;
79    public static final int VIEW_TYPE_MESSAGE_FOOTER = 2;
80    public static final int VIEW_TYPE_SUPER_COLLAPSED_BLOCK = 3;
81    public static final int VIEW_TYPE_COUNT = 4;
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 class MessageHeaderItem extends ConversationOverlayItem {
124        private ConversationMessage mMessage;
125
126        // view state variables
127        private boolean mExpanded;
128        public boolean detailsExpanded;
129        private boolean mShowImages;
130
131        // cached values to speed up re-rendering during view recycling
132        public CharSequence timestampShort;
133        public CharSequence timestampLong;
134        public CharSequence recipientSummaryText;
135
136        MessageHeaderItem(ConversationMessage message, boolean expanded, boolean showImages) {
137            mMessage = message;
138            mExpanded = expanded;
139            mShowImages = showImages;
140
141            detailsExpanded = false;
142        }
143
144        public ConversationMessage getMessage() {
145            return mMessage;
146        }
147
148        @Override
149        public int getType() {
150            return VIEW_TYPE_MESSAGE_HEADER;
151        }
152
153        @Override
154        public View createView(Context context, LayoutInflater inflater, ViewGroup parent) {
155            final MessageHeaderView v = (MessageHeaderView) inflater.inflate(
156                    R.layout.conversation_message_header, parent, false);
157            v.initialize(mDateBuilder, mAccountController, mAddressCache);
158            v.setCallbacks(mMessageCallbacks);
159            v.setContactInfoSource(mContactInfoSource);
160            v.setVeiledMatcher(mMatcher);
161            return v;
162        }
163
164        @Override
165        public void bindView(View v, boolean measureOnly) {
166            final MessageHeaderView header = (MessageHeaderView) v;
167            header.bind(this, measureOnly);
168        }
169
170        @Override
171        public void onModelUpdated(View v) {
172            final MessageHeaderView header = (MessageHeaderView) v;
173            header.refresh();
174        }
175
176        @Override
177        public boolean isContiguous() {
178            return !isExpanded();
179        }
180
181        public boolean isExpanded() {
182            return mExpanded;
183        }
184
185        public void setExpanded(boolean expanded) {
186            if (mExpanded != expanded) {
187                mExpanded = expanded;
188            }
189        }
190
191        public boolean getShowImages() {
192            return mShowImages;
193        }
194
195        public void setShowImages(boolean showImages) {
196            mShowImages = showImages;
197        }
198
199        @Override
200        public boolean canBecomeSnapHeader() {
201            return isExpanded();
202        }
203
204        @Override
205        public boolean canPushSnapHeader() {
206            return true;
207        }
208
209        @Override
210        public boolean belongsToMessage(ConversationMessage message) {
211            return Objects.equal(mMessage, message);
212        }
213
214        @Override
215        public void setMessage(ConversationMessage message) {
216            mMessage = message;
217        }
218
219    }
220
221    public class MessageFooterItem extends ConversationOverlayItem {
222        /**
223         * A footer can only exist if there is a matching header. Requiring a header allows a
224         * footer to stay in sync with the expanded state of the header.
225         */
226        private final MessageHeaderItem headerItem;
227
228        private MessageFooterItem(MessageHeaderItem item) {
229            headerItem = item;
230        }
231
232        @Override
233        public int getType() {
234            return VIEW_TYPE_MESSAGE_FOOTER;
235        }
236
237        @Override
238        public View createView(Context context, LayoutInflater inflater, ViewGroup parent) {
239            final MessageFooterView v = (MessageFooterView) inflater.inflate(
240                    R.layout.conversation_message_footer, parent, false);
241            v.initialize(mLoaderManager, mFragmentManager);
242            return v;
243        }
244
245        @Override
246        public void bindView(View v, boolean measureOnly) {
247            final MessageFooterView attachmentsView = (MessageFooterView) v;
248            attachmentsView.bind(headerItem, measureOnly);
249        }
250
251        @Override
252        public boolean isContiguous() {
253            return true;
254        }
255
256        @Override
257        public int getGravity() {
258            // attachments are top-aligned within their spacer area
259            // Attachments should stay near the body they belong to, even when zoomed far in.
260            return Gravity.TOP;
261        }
262
263        @Override
264        public int getHeight() {
265            // a footer may change height while its view does not exist because it is offscreen
266            // (but the header is onscreen and thus collapsible)
267            if (!headerItem.isExpanded()) {
268                return 0;
269            }
270            return super.getHeight();
271        }
272    }
273
274    public class SuperCollapsedBlockItem extends ConversationOverlayItem {
275
276        private final int mStart;
277        private int mEnd;
278
279        private SuperCollapsedBlockItem(int start, int end) {
280            mStart = start;
281            mEnd = end;
282        }
283
284        @Override
285        public int getType() {
286            return VIEW_TYPE_SUPER_COLLAPSED_BLOCK;
287        }
288
289        @Override
290        public View createView(Context context, LayoutInflater inflater, ViewGroup parent) {
291            final SuperCollapsedBlock scb = (SuperCollapsedBlock) inflater.inflate(
292                    R.layout.super_collapsed_block, parent, false);
293            scb.initialize(mSuperCollapsedListener);
294            return scb;
295        }
296
297        @Override
298        public void bindView(View v, boolean measureOnly) {
299            final SuperCollapsedBlock scb = (SuperCollapsedBlock) v;
300            scb.bind(this);
301        }
302
303        @Override
304        public boolean isContiguous() {
305            return true;
306        }
307
308        public int getStart() {
309            return mStart;
310        }
311
312        public int getEnd() {
313            return mEnd;
314        }
315
316        @Override
317        public boolean canPushSnapHeader() {
318            return true;
319        }
320    }
321
322    public ConversationViewAdapter(ControllableActivity controllableActivity,
323            ConversationAccountController accountController,
324            LoaderManager loaderManager,
325            MessageHeaderViewCallbacks messageCallbacks,
326            ContactInfoSource contactInfoSource,
327            ConversationViewHeaderCallbacks convCallbacks,
328            SuperCollapsedBlock.OnClickListener scbListener, Map<String, Address> addressCache,
329            FormattedDateBuilder dateBuilder) {
330        mContext = controllableActivity.getActivityContext();
331        mDateBuilder = dateBuilder;
332        mAccountController = accountController;
333        mLoaderManager = loaderManager;
334        mFragmentManager = controllableActivity.getFragmentManager();
335        mMessageCallbacks = messageCallbacks;
336        mContactInfoSource = contactInfoSource;
337        mConversationCallbacks = convCallbacks;
338        mSuperCollapsedListener = scbListener;
339        mAddressCache = addressCache;
340        mInflater = LayoutInflater.from(mContext);
341
342        mItems = Lists.newArrayList();
343        mMatcher = controllableActivity.getAccountController().getVeiledAddressMatcher();
344    }
345
346    @Override
347    public int getCount() {
348        return mItems.size();
349    }
350
351    @Override
352    public int getItemViewType(int position) {
353        return mItems.get(position).getType();
354    }
355
356    @Override
357    public int getViewTypeCount() {
358        return VIEW_TYPE_COUNT;
359    }
360
361    @Override
362    public ConversationOverlayItem getItem(int position) {
363        return mItems.get(position);
364    }
365
366    @Override
367    public long getItemId(int position) {
368        return position; // TODO: ensure this works well enough
369    }
370
371    @Override
372    public View getView(int position, View convertView, ViewGroup parent) {
373        return getView(getItem(position), convertView, parent, false /* measureOnly */);
374    }
375
376    public View getView(ConversationOverlayItem item, View convertView, ViewGroup parent,
377            boolean measureOnly) {
378        final View v;
379
380        if (convertView == null) {
381            v = item.createView(mContext, mInflater, parent);
382        } else {
383            v = convertView;
384        }
385        item.bindView(v, measureOnly);
386
387        return v;
388    }
389
390    public int addItem(ConversationOverlayItem item) {
391        final int pos = mItems.size();
392        mItems.add(item);
393        return pos;
394    }
395
396    public void clear() {
397        mItems.clear();
398        notifyDataSetChanged();
399    }
400
401    public int addConversationHeader(Conversation conv) {
402        return addItem(new ConversationHeaderItem(conv));
403    }
404
405    public int addMessageHeader(ConversationMessage msg, boolean expanded, boolean showImages) {
406        return addItem(new MessageHeaderItem(msg, expanded, showImages));
407    }
408
409    public int addMessageFooter(MessageHeaderItem headerItem) {
410        return addItem(new MessageFooterItem(headerItem));
411    }
412
413    public MessageHeaderItem newMessageHeaderItem(ConversationMessage message, boolean expanded,
414            boolean showImages) {
415        return new MessageHeaderItem(message, expanded, showImages);
416    }
417
418    public MessageFooterItem newMessageFooterItem(MessageHeaderItem headerItem) {
419        return new MessageFooterItem(headerItem);
420    }
421
422    public int addSuperCollapsedBlock(int start, int end) {
423        return addItem(new SuperCollapsedBlockItem(start, end));
424    }
425
426    public void replaceSuperCollapsedBlock(SuperCollapsedBlockItem blockToRemove,
427            Collection<ConversationOverlayItem> replacements) {
428        final int pos = mItems.indexOf(blockToRemove);
429        if (pos == -1) {
430            return;
431        }
432
433        mItems.remove(pos);
434        mItems.addAll(pos, replacements);
435    }
436
437    public void updateItemsForMessage(ConversationMessage message,
438            List<Integer> affectedPositions) {
439        for (int i = 0, len = mItems.size(); i < len; i++) {
440            final ConversationOverlayItem item = mItems.get(i);
441            if (item.belongsToMessage(message)) {
442                item.setMessage(message);
443                affectedPositions.add(i);
444            }
445        }
446    }
447
448}
449