ConversationViewAdapter.java revision 8081df46ef5a7794374e41cd1836e778a2da9b31
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.LayoutInflater;
24import android.view.View;
25import android.view.ViewGroup;
26import android.widget.BaseAdapter;
27
28import com.android.mail.ContactInfoSource;
29import com.android.mail.FormattedDateBuilder;
30import com.android.mail.R;
31import com.android.mail.browse.ConversationViewHeader.ConversationViewHeaderCallbacks;
32import com.android.mail.browse.MessageCursor.ConversationMessage;
33import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks;
34import com.android.mail.browse.SuperCollapsedBlock.OnClickListener;
35import com.android.mail.providers.Account;
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.google.common.collect.Lists;
41
42import java.util.Collection;
43import java.util.List;
44import java.util.Map;
45
46/**
47 * A specialized adapter that contains overlay views to draw on top of the underlying conversation
48 * WebView. Each independently drawn overlay view gets its own item in this adapter, and indices
49 * in this adapter do not necessarily line up with cursor indices. For example, an expanded
50 * message may have a header and footer, and since they are not drawn coupled together, they each
51 * get an adapter item.
52 * <p>
53 * Each item in this adapter is a {@link ConversationOverlayItem} to expose enough information
54 * to {@link ConversationContainer} so that it can position overlays properly.
55 *
56 */
57public class ConversationViewAdapter extends BaseAdapter {
58
59    private Context mContext;
60    private final FormattedDateBuilder mDateBuilder;
61    private final ConversationAccountController mAccountController;
62    private final LoaderManager mLoaderManager;
63    private final FragmentManager mFragmentManager;
64    private final MessageHeaderViewCallbacks mMessageCallbacks;
65    private final ContactInfoSource mContactInfoSource;
66    private ConversationViewHeaderCallbacks mConversationCallbacks;
67    private OnClickListener mSuperCollapsedListener;
68    private Map<String, Address> mAddressCache;
69    private final LayoutInflater mInflater;
70
71    private final List<ConversationOverlayItem> mItems;
72
73    public static final int VIEW_TYPE_CONVERSATION_HEADER = 0;
74    public static final int VIEW_TYPE_MESSAGE_HEADER = 1;
75    public static final int VIEW_TYPE_MESSAGE_FOOTER = 2;
76    public static final int VIEW_TYPE_SUPER_COLLAPSED_BLOCK = 3;
77    public static final int VIEW_TYPE_COUNT = 4;
78
79    public interface ConversationAccountController {
80        Account getAccount();
81    }
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
101            headerView.setSubject(mConversation.subject, false /* notify */);
102            if (mAccountController.getAccount().supportsCapability(
103                    UIProvider.AccountCapabilities.MULTIPLE_FOLDERS_PER_CONV)) {
104                headerView.setFolders(mConversation, false /* notify */);
105            }
106
107            return headerView;
108        }
109
110        @Override
111        public void bindView(View v, boolean measureOnly) {
112            // There is only one conversation header, so the work is done once in createView.
113        }
114
115        @Override
116        public boolean isContiguous() {
117            return true;
118        }
119
120    }
121
122    public class MessageHeaderItem extends ConversationOverlayItem {
123        public final ConversationMessage message;
124
125        // view state variables
126        private boolean mExpanded;
127        public boolean detailsExpanded;
128
129        // cached values to speed up re-rendering during view recycling
130        public CharSequence timestampShort;
131        public CharSequence timestampLong;
132        public CharSequence recipientSummaryText;
133
134        private MessageHeaderItem(ConversationMessage message, boolean expanded) {
135            this.message = message;
136            mExpanded = expanded;
137
138            detailsExpanded = false;
139        }
140
141        @Override
142        public int getType() {
143            return VIEW_TYPE_MESSAGE_HEADER;
144        }
145
146        @Override
147        public View createView(Context context, LayoutInflater inflater, ViewGroup parent) {
148            final MessageHeaderView v = (MessageHeaderView) inflater.inflate(
149                    R.layout.conversation_message_header, parent, false);
150            v.initialize(mDateBuilder, mAccountController, mAddressCache);
151            v.setCallbacks(mMessageCallbacks);
152            v.setContactInfoSource(mContactInfoSource);
153            return v;
154        }
155
156        @Override
157        public void bindView(View v, boolean measureOnly) {
158            final MessageHeaderView header = (MessageHeaderView) v;
159            header.bind(this, measureOnly);
160        }
161
162        @Override
163        public boolean isContiguous() {
164            return !isExpanded();
165        }
166
167        public boolean isExpanded() {
168            return mExpanded;
169        }
170
171        public void setExpanded(boolean expanded) {
172            if (mExpanded != expanded) {
173                mExpanded = expanded;
174            }
175        }
176
177        @Override
178        public boolean canBecomeSnapHeader() {
179            return isExpanded();
180        }
181
182        @Override
183        public boolean canPushSnapHeader() {
184            return true;
185        }
186
187    }
188
189    public class MessageFooterItem extends ConversationOverlayItem {
190        /**
191         * A footer can only exist if there is a matching header. Requiring a header allows a
192         * footer to stay in sync with the expanded state of the header.
193         */
194        private final MessageHeaderItem headerItem;
195
196        private MessageFooterItem(MessageHeaderItem item) {
197            headerItem = item;
198        }
199
200        @Override
201        public int getType() {
202            return VIEW_TYPE_MESSAGE_FOOTER;
203        }
204
205        @Override
206        public View createView(Context context, LayoutInflater inflater, ViewGroup parent) {
207            final MessageFooterView v = (MessageFooterView) inflater.inflate(
208                    R.layout.conversation_message_footer, parent, false);
209            v.initialize(mLoaderManager, mFragmentManager);
210            return v;
211        }
212
213        @Override
214        public void bindView(View v, boolean measureOnly) {
215            final MessageFooterView attachmentsView = (MessageFooterView) v;
216            attachmentsView.bind(headerItem, measureOnly);
217        }
218
219        @Override
220        public boolean isContiguous() {
221            return true;
222        }
223
224        @Override
225        public int getHeight() {
226            // a footer may change height while its view does not exist because it is offscreen
227            // (but the header is onscreen and thus collapsible)
228            if (!headerItem.isExpanded()) {
229                return 0;
230            }
231            return super.getHeight();
232        }
233    }
234
235    public class SuperCollapsedBlockItem extends ConversationOverlayItem {
236
237        private final int mStart;
238        private int mEnd;
239
240        private SuperCollapsedBlockItem(int start, int end) {
241            mStart = start;
242            mEnd = end;
243        }
244
245        @Override
246        public int getType() {
247            return VIEW_TYPE_SUPER_COLLAPSED_BLOCK;
248        }
249
250        @Override
251        public View createView(Context context, LayoutInflater inflater, ViewGroup parent) {
252            final SuperCollapsedBlock scb = (SuperCollapsedBlock) inflater.inflate(
253                    R.layout.super_collapsed_block, parent, false);
254            scb.initialize(mSuperCollapsedListener);
255            return scb;
256        }
257
258        @Override
259        public void bindView(View v, boolean measureOnly) {
260            final SuperCollapsedBlock scb = (SuperCollapsedBlock) v;
261            scb.bind(this);
262        }
263
264        @Override
265        public boolean isContiguous() {
266            return true;
267        }
268
269        public int getStart() {
270            return mStart;
271        }
272
273        public int getEnd() {
274            return mEnd;
275        }
276
277        @Override
278        public boolean canPushSnapHeader() {
279            return true;
280        }
281    }
282
283    public ConversationViewAdapter(ControllableActivity controllableActivity,
284            ConversationAccountController accountController,
285            LoaderManager loaderManager,
286            MessageHeaderViewCallbacks messageCallbacks,
287            ContactInfoSource contactInfoSource,
288            ConversationViewHeaderCallbacks convCallbacks,
289            SuperCollapsedBlock.OnClickListener scbListener, Map<String, Address> addressCache,
290            FormattedDateBuilder dateBuilder) {
291        mContext = controllableActivity.getActivityContext();
292        mDateBuilder = dateBuilder;
293        mAccountController = accountController;
294        mLoaderManager = loaderManager;
295        mFragmentManager = controllableActivity.getFragmentManager();
296        mMessageCallbacks = messageCallbacks;
297        mContactInfoSource = contactInfoSource;
298        mConversationCallbacks = convCallbacks;
299        mSuperCollapsedListener = scbListener;
300        mAddressCache = addressCache;
301        mInflater = LayoutInflater.from(mContext);
302
303        mItems = Lists.newArrayList();
304    }
305
306    @Override
307    public int getCount() {
308        return mItems.size();
309    }
310
311    @Override
312    public int getItemViewType(int position) {
313        return mItems.get(position).getType();
314    }
315
316    @Override
317    public int getViewTypeCount() {
318        return VIEW_TYPE_COUNT;
319    }
320
321    @Override
322    public ConversationOverlayItem getItem(int position) {
323        return mItems.get(position);
324    }
325
326    @Override
327    public long getItemId(int position) {
328        return position; // TODO: ensure this works well enough
329    }
330
331    @Override
332    public View getView(int position, View convertView, ViewGroup parent) {
333        return getView(getItem(position), convertView, parent, false /* measureOnly */);
334    }
335
336    public View getView(ConversationOverlayItem item, View convertView, ViewGroup parent,
337            boolean measureOnly) {
338        final View v;
339
340        if (convertView == null) {
341            v = item.createView(mContext, mInflater, parent);
342        } else {
343            v = convertView;
344        }
345        item.bindView(v, measureOnly);
346
347        return v;
348    }
349
350    public int addItem(ConversationOverlayItem item) {
351        final int pos = mItems.size();
352        mItems.add(item);
353        notifyDataSetChanged();
354        return pos;
355    }
356
357    public void clear() {
358        mItems.clear();
359        notifyDataSetChanged();
360    }
361
362    public int addConversationHeader(Conversation conv) {
363        return addItem(new ConversationHeaderItem(conv));
364    }
365
366    public int addMessageHeader(ConversationMessage msg, boolean expanded) {
367        return addItem(new MessageHeaderItem(msg, expanded));
368    }
369
370    public int addMessageFooter(MessageHeaderItem headerItem) {
371        return addItem(new MessageFooterItem(headerItem));
372    }
373
374    public MessageHeaderItem newMessageHeaderItem(ConversationMessage message, boolean expanded) {
375        return new MessageHeaderItem(message, expanded);
376    }
377
378    public MessageFooterItem newMessageFooterItem(MessageHeaderItem headerItem) {
379        return new MessageFooterItem(headerItem);
380    }
381
382    public int addSuperCollapsedBlock(int start, int end) {
383        return addItem(new SuperCollapsedBlockItem(start, end));
384    }
385
386    public void replaceSuperCollapsedBlock(SuperCollapsedBlockItem blockToRemove,
387            Collection<ConversationOverlayItem> replacements) {
388        final int pos = mItems.indexOf(blockToRemove);
389        if (pos == -1) {
390            return;
391        }
392
393        mItems.remove(pos);
394        mItems.addAll(pos, replacements);
395        notifyDataSetChanged();
396    }
397
398}
399