Home | History | Annotate | Download | only in browse
      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 
     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