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