• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
18 package com.android.mail.browse;
19 
20 import android.app.FragmentManager;
21 import android.app.LoaderManager;
22 import android.content.Context;
23 import android.view.Gravity;
24 import android.view.LayoutInflater;
25 import android.view.View;
26 import android.view.ViewGroup;
27 import android.widget.BaseAdapter;
28 
29 import com.android.mail.ContactInfoSource;
30 import com.android.mail.FormattedDateBuilder;
31 import com.android.mail.R;
32 import com.android.mail.browse.ConversationViewHeader.ConversationViewHeaderCallbacks;
33 import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks;
34 import com.android.mail.browse.SuperCollapsedBlock.OnClickListener;
35 import com.android.mail.providers.Address;
36 import com.android.mail.providers.Conversation;
37 import com.android.mail.providers.UIProvider;
38 import com.android.mail.ui.ControllableActivity;
39 import com.android.mail.utils.VeiledAddressMatcher;
40 import com.google.common.base.Objects;
41 import com.google.common.collect.Lists;
42 
43 import java.util.Collection;
44 import java.util.List;
45 import 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  */
58 public 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 
ConversationHeaderItem(Conversation conv)88         private ConversationHeaderItem(Conversation conv) {
89             mConversation = conv;
90         }
91 
92         @Override
getType()93         public int getType() {
94             return VIEW_TYPE_CONVERSATION_HEADER;
95         }
96 
97         @Override
createView(Context context, LayoutInflater inflater, ViewGroup parent)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
bindView(View v, boolean measureOnly)113         public void bindView(View v, boolean measureOnly) {
114             ConversationViewHeader header = (ConversationViewHeader) v;
115             header.bind(this);
116         }
117 
118         @Override
isContiguous()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 
MessageHeaderItem(ConversationViewAdapter adapter, FormattedDateBuilder dateBuilder, ConversationMessage message, boolean expanded, boolean showImages)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 
getMessage()154         public ConversationMessage getMessage() {
155             return mMessage;
156         }
157 
158         @Override
getType()159         public int getType() {
160             return VIEW_TYPE_MESSAGE_HEADER;
161         }
162 
163         @Override
createView(Context context, LayoutInflater inflater, ViewGroup parent)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
bindView(View v, boolean measureOnly)176         public void bindView(View v, boolean measureOnly) {
177             final MessageHeaderView header = (MessageHeaderView) v;
178             header.bind(this, measureOnly);
179         }
180 
181         @Override
onModelUpdated(View v)182         public void onModelUpdated(View v) {
183             final MessageHeaderView header = (MessageHeaderView) v;
184             header.refresh();
185         }
186 
187         @Override
isContiguous()188         public boolean isContiguous() {
189             return !isExpanded();
190         }
191 
192         @Override
isExpanded()193         public boolean isExpanded() {
194             return mExpanded;
195         }
196 
setExpanded(boolean expanded)197         public void setExpanded(boolean expanded) {
198             if (mExpanded != expanded) {
199                 mExpanded = expanded;
200             }
201         }
202 
getShowImages()203         public boolean getShowImages() {
204             return mShowImages;
205         }
206 
setShowImages(boolean showImages)207         public void setShowImages(boolean showImages) {
208             mShowImages = showImages;
209         }
210 
211         @Override
canBecomeSnapHeader()212         public boolean canBecomeSnapHeader() {
213             return isExpanded();
214         }
215 
216         @Override
canPushSnapHeader()217         public boolean canPushSnapHeader() {
218             return true;
219         }
220 
221         @Override
belongsToMessage(ConversationMessage message)222         public boolean belongsToMessage(ConversationMessage message) {
223             return Objects.equal(mMessage, message);
224         }
225 
226         @Override
setMessage(ConversationMessage message)227         public void setMessage(ConversationMessage message) {
228             mMessage = message;
229         }
230 
getTimestampShort()231         public CharSequence getTimestampShort() {
232             ensureTimestamps();
233             return mTimestampShort;
234         }
235 
getTimestampLong()236         public CharSequence getTimestampLong() {
237             ensureTimestamps();
238             return mTimestampLong;
239         }
240 
ensureTimestamps()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 
getAdapter()249         public ConversationViewAdapter getAdapter() {
250             return mAdapter;
251         }
252 
253         @Override
rebindView(View view)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 
MessageFooterItem(MessageHeaderItem item)267         private MessageFooterItem(MessageHeaderItem item) {
268             mHeaderitem = item;
269         }
270 
271         @Override
getType()272         public int getType() {
273             return VIEW_TYPE_MESSAGE_FOOTER;
274         }
275 
276         @Override
createView(Context context, LayoutInflater inflater, ViewGroup parent)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
bindView(View v, boolean measureOnly)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
isContiguous()291         public boolean isContiguous() {
292             return true;
293         }
294 
295         @Override
isExpanded()296         public boolean isExpanded() {
297             return mHeaderitem.isExpanded();
298         }
299 
300         @Override
getGravity()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
getHeight()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 
SuperCollapsedBlockItem(int start, int end)323         private SuperCollapsedBlockItem(int start, int end) {
324             mStart = start;
325             mEnd = end;
326         }
327 
328         @Override
getType()329         public int getType() {
330             return VIEW_TYPE_SUPER_COLLAPSED_BLOCK;
331         }
332 
333         @Override
createView(Context context, LayoutInflater inflater, ViewGroup parent)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
bindView(View v, boolean measureOnly)342         public void bindView(View v, boolean measureOnly) {
343             final SuperCollapsedBlock scb = (SuperCollapsedBlock) v;
344             scb.bind(this);
345         }
346 
347         @Override
isContiguous()348         public boolean isContiguous() {
349             return true;
350         }
351 
352         @Override
isExpanded()353         public boolean isExpanded() {
354             return false;
355         }
356 
getStart()357         public int getStart() {
358             return mStart;
359         }
360 
getEnd()361         public int getEnd() {
362             return mEnd;
363         }
364 
365         @Override
canPushSnapHeader()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 
BorderItem(boolean contiguous, boolean isExpanded, boolean firstBorder, boolean lastBorder)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
getType()387         public int getType() {
388             return VIEW_TYPE_BORDER;
389         }
390 
391         @Override
createView(Context context, LayoutInflater inflater, ViewGroup parent)392         public View createView(Context context, LayoutInflater inflater, ViewGroup parent) {
393             return inflater.inflate(R.layout.card_border, parent, false);
394         }
395 
396         @Override
bindView(View v, boolean measureOnly)397         public void bindView(View v, boolean measureOnly) {
398             final BorderView border = (BorderView) v;
399             border.bind(this, measureOnly);
400         }
401 
402         @Override
isContiguous()403         public boolean isContiguous() {
404             return mContiguous;
405         }
406 
407         @Override
isExpanded()408         public boolean isExpanded() {
409             return mExpanded;
410         }
411 
setExpanded(boolean isExpanded)412         public void setExpanded(boolean isExpanded) {
413             mExpanded = isExpanded;
414         }
415 
416         @Override
canPushSnapHeader()417         public boolean canPushSnapHeader() {
418             return false;
419         }
420 
isFirstBorder()421         public boolean isFirstBorder() {
422             return mFirstBorder;
423         }
424 
isLastBorder()425         public boolean isLastBorder() {
426             return mLastBorder;
427         }
428 
setIsLastBorder(boolean isLastBorder)429         public void setIsLastBorder(boolean isLastBorder) {
430             mLastBorder = isLastBorder;
431         }
432 
getAdapter()433         public ConversationViewAdapter getAdapter() {
434             return ConversationViewAdapter.this;
435         }
436 
437         @Override
rebindView(View view)438         public void rebindView(View view) {
439             bindView(view, false);
440         }
441     }
442 
ConversationViewAdapter(ControllableActivity controllableActivity, ConversationAccountController accountController, LoaderManager loaderManager, MessageHeaderViewCallbacks messageCallbacks, ContactInfoSource contactInfoSource, ConversationViewHeaderCallbacks convCallbacks, SuperCollapsedBlock.OnClickListener scbListener, Map<String, Address> addressCache, FormattedDateBuilder dateBuilder)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
getCount()468     public int getCount() {
469         return mItems.size();
470     }
471 
472     @Override
getItemViewType(int position)473     public int getItemViewType(int position) {
474         return mItems.get(position).getType();
475     }
476 
477     @Override
getViewTypeCount()478     public int getViewTypeCount() {
479         return VIEW_TYPE_COUNT;
480     }
481 
482     @Override
getItem(int position)483     public ConversationOverlayItem getItem(int position) {
484         return mItems.get(position);
485     }
486 
487     @Override
getItemId(int position)488     public long getItemId(int position) {
489         return position; // TODO: ensure this works well enough
490     }
491 
492     @Override
getView(int position, View convertView, ViewGroup parent)493     public View getView(int position, View convertView, ViewGroup parent) {
494         return getView(getItem(position), convertView, parent, false /* measureOnly */);
495     }
496 
getView(ConversationOverlayItem item, View convertView, ViewGroup parent, boolean measureOnly)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 
getLayoutInflater()511     public LayoutInflater getLayoutInflater() {
512         return mInflater;
513     }
514 
getDateBuilder()515     public FormattedDateBuilder getDateBuilder() {
516         return mDateBuilder;
517     }
518 
addItem(ConversationOverlayItem item)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 
clear()526     public void clear() {
527         mItems.clear();
528         notifyDataSetChanged();
529     }
530 
addConversationHeader(Conversation conv)531     public int addConversationHeader(Conversation conv) {
532         return addItem(new ConversationHeaderItem(conv));
533     }
534 
addMessageHeader(ConversationMessage msg, boolean expanded, boolean showImages)535     public int addMessageHeader(ConversationMessage msg, boolean expanded, boolean showImages) {
536         return addItem(new MessageHeaderItem(this, mDateBuilder, msg, expanded, showImages));
537     }
538 
addMessageFooter(MessageHeaderItem headerItem)539     public int addMessageFooter(MessageHeaderItem headerItem) {
540         return addItem(new MessageFooterItem(headerItem));
541     }
542 
newMessageHeaderItem(ConversationViewAdapter adapter, FormattedDateBuilder dateBuilder, ConversationMessage message, boolean expanded, boolean showImages)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 
newMessageFooterItem(MessageHeaderItem headerItem)549     public MessageFooterItem newMessageFooterItem(MessageHeaderItem headerItem) {
550         return new MessageFooterItem(headerItem);
551     }
552 
addSuperCollapsedBlock(int start, int end)553     public int addSuperCollapsedBlock(int start, int end) {
554         return addItem(new SuperCollapsedBlockItem(start, end));
555     }
556 
addBorder( boolean contiguous, boolean expanded, boolean firstBorder, boolean lastBorder)557     public int addBorder(
558             boolean contiguous, boolean expanded, boolean firstBorder, boolean lastBorder) {
559         return addItem(new BorderItem(contiguous, expanded, firstBorder, lastBorder));
560     }
561 
newBorderItem(boolean contiguous, boolean expanded)562     public BorderItem newBorderItem(boolean contiguous, boolean expanded) {
563         return new BorderItem(
564                 contiguous, expanded, false /* firstBorder */, false /* lastBorder */);
565     }
566 
replaceSuperCollapsedBlock(SuperCollapsedBlockItem blockToRemove, Collection<ConversationOverlayItem> replacements)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 
updateItemsForMessage(ConversationMessage message, List<Integer> affectedPositions)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