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