ConversationViewAdapter.java revision cee3c90574b48ccaa0f8b9f9341383c231ed41d2
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.MessageHeaderView.MessageHeaderViewCallbacks; 34import com.android.mail.browse.SuperCollapsedBlock.OnClickListener; 35import com.android.mail.providers.Address; 36import com.android.mail.providers.Conversation; 37import com.android.mail.providers.UIProvider; 38import com.android.mail.ui.ControllableActivity; 39import com.android.mail.utils.VeiledAddressMatcher; 40import com.google.common.base.Objects; 41import com.google.common.collect.Lists; 42 43import java.util.Collection; 44import java.util.List; 45import 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 */ 58public 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_AD_HEADER = 1; 77 public static final int VIEW_TYPE_MESSAGE_HEADER = 2; 78 public static final int VIEW_TYPE_MESSAGE_FOOTER = 3; 79 public static final int VIEW_TYPE_SUPER_COLLAPSED_BLOCK = 4; 80 public static final int VIEW_TYPE_BORDER = 5; 81 public static final int VIEW_TYPE_COUNT = 6; 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 static class MessageHeaderItem extends ConversationOverlayItem { 124 125 private final ConversationViewAdapter mAdapter; 126 127 private ConversationMessage mMessage; 128 129 // view state variables 130 private boolean mExpanded; 131 public boolean detailsExpanded; 132 private boolean mShowImages; 133 134 // cached values to speed up re-rendering during view recycling 135 private CharSequence mTimestampShort; 136 private CharSequence mTimestampLong; 137 private long mTimestampMs; 138 private FormattedDateBuilder mDateBuilder; 139 public CharSequence recipientSummaryText; 140 141 MessageHeaderItem(ConversationViewAdapter adapter, FormattedDateBuilder dateBuilder, 142 ConversationMessage message, boolean expanded, boolean showImages) { 143 mAdapter = adapter; 144 mDateBuilder = dateBuilder; 145 mMessage = message; 146 mExpanded = expanded; 147 mShowImages = showImages; 148 149 detailsExpanded = false; 150 } 151 152 public ConversationMessage getMessage() { 153 return mMessage; 154 } 155 156 @Override 157 public int getType() { 158 return VIEW_TYPE_MESSAGE_HEADER; 159 } 160 161 @Override 162 public View createView(Context context, LayoutInflater inflater, ViewGroup parent) { 163 final MessageHeaderView v = (MessageHeaderView) inflater.inflate( 164 R.layout.conversation_message_header, parent, false); 165 v.initialize(mAdapter.mAccountController, 166 mAdapter.mAddressCache); 167 v.setCallbacks(mAdapter.mMessageCallbacks); 168 v.setContactInfoSource(mAdapter.mContactInfoSource); 169 v.setVeiledMatcher(mAdapter.mMatcher); 170 return v; 171 } 172 173 @Override 174 public void bindView(View v, boolean measureOnly) { 175 final MessageHeaderView header = (MessageHeaderView) v; 176 header.bind(this, measureOnly); 177 } 178 179 @Override 180 public void onModelUpdated(View v) { 181 final MessageHeaderView header = (MessageHeaderView) v; 182 header.refresh(); 183 } 184 185 @Override 186 public boolean isContiguous() { 187 return !isExpanded(); 188 } 189 190 @Override 191 public boolean isExpanded() { 192 return mExpanded; 193 } 194 195 public void setExpanded(boolean expanded) { 196 if (mExpanded != expanded) { 197 mExpanded = expanded; 198 } 199 } 200 201 public boolean getShowImages() { 202 return mShowImages; 203 } 204 205 public void setShowImages(boolean showImages) { 206 mShowImages = showImages; 207 } 208 209 @Override 210 public boolean canBecomeSnapHeader() { 211 return isExpanded(); 212 } 213 214 @Override 215 public boolean canPushSnapHeader() { 216 return true; 217 } 218 219 @Override 220 public boolean belongsToMessage(ConversationMessage message) { 221 return Objects.equal(mMessage, message); 222 } 223 224 @Override 225 public void setMessage(ConversationMessage message) { 226 mMessage = message; 227 } 228 229 public CharSequence getTimestampShort() { 230 ensureTimestamps(); 231 return mTimestampShort; 232 } 233 234 public CharSequence getTimestampLong() { 235 ensureTimestamps(); 236 return mTimestampLong; 237 } 238 239 private void ensureTimestamps() { 240 if (mMessage.dateReceivedMs != mTimestampMs) { 241 mTimestampMs = mMessage.dateReceivedMs; 242 mTimestampShort = mDateBuilder.formatShortDate(mTimestampMs); 243 mTimestampLong = mDateBuilder.formatLongDateTime(mTimestampMs); 244 } 245 } 246 247 public ConversationViewAdapter getAdapter() { 248 return mAdapter; 249 } 250 } 251 252 public class MessageFooterItem extends ConversationOverlayItem { 253 /** 254 * A footer can only exist if there is a matching header. Requiring a header allows a 255 * footer to stay in sync with the expanded state of the header. 256 */ 257 private final MessageHeaderItem mHeaderitem; 258 259 private MessageFooterItem(MessageHeaderItem item) { 260 mHeaderitem = item; 261 } 262 263 @Override 264 public int getType() { 265 return VIEW_TYPE_MESSAGE_FOOTER; 266 } 267 268 @Override 269 public View createView(Context context, LayoutInflater inflater, ViewGroup parent) { 270 final MessageFooterView v = (MessageFooterView) inflater.inflate( 271 R.layout.conversation_message_footer, parent, false); 272 v.initialize(mLoaderManager, mFragmentManager); 273 return v; 274 } 275 276 @Override 277 public void bindView(View v, boolean measureOnly) { 278 final MessageFooterView attachmentsView = (MessageFooterView) v; 279 attachmentsView.bind(mHeaderitem, mAccountController.getAccount().uri, measureOnly); 280 } 281 282 @Override 283 public boolean isContiguous() { 284 return true; 285 } 286 287 @Override 288 public boolean isExpanded() { 289 return mHeaderitem.isExpanded(); 290 } 291 292 @Override 293 public int getGravity() { 294 // attachments are top-aligned within their spacer area 295 // Attachments should stay near the body they belong to, even when zoomed far in. 296 return Gravity.TOP; 297 } 298 299 @Override 300 public int getHeight() { 301 // a footer may change height while its view does not exist because it is offscreen 302 // (but the header is onscreen and thus collapsible) 303 if (!mHeaderitem.isExpanded()) { 304 return 0; 305 } 306 return super.getHeight(); 307 } 308 } 309 310 public class SuperCollapsedBlockItem extends ConversationOverlayItem { 311 312 private final int mStart; 313 private int mEnd; 314 315 private SuperCollapsedBlockItem(int start, int end) { 316 mStart = start; 317 mEnd = end; 318 } 319 320 @Override 321 public int getType() { 322 return VIEW_TYPE_SUPER_COLLAPSED_BLOCK; 323 } 324 325 @Override 326 public View createView(Context context, LayoutInflater inflater, ViewGroup parent) { 327 final SuperCollapsedBlock scb = (SuperCollapsedBlock) inflater.inflate( 328 R.layout.super_collapsed_block, parent, false); 329 scb.initialize(mSuperCollapsedListener); 330 return scb; 331 } 332 333 @Override 334 public void bindView(View v, boolean measureOnly) { 335 final SuperCollapsedBlock scb = (SuperCollapsedBlock) v; 336 scb.bind(this); 337 } 338 339 @Override 340 public boolean isContiguous() { 341 return true; 342 } 343 344 @Override 345 public boolean isExpanded() { 346 return false; 347 } 348 349 public int getStart() { 350 return mStart; 351 } 352 353 public int getEnd() { 354 return mEnd; 355 } 356 357 @Override 358 public boolean canPushSnapHeader() { 359 return true; 360 } 361 } 362 363 364 public class BorderItem extends ConversationOverlayItem { 365 private final boolean mContiguous; 366 private boolean mExpanded; 367 private final boolean mFirstBorder; 368 private boolean mLastBorder; 369 370 public BorderItem(boolean contiguous, boolean isExpanded, 371 boolean firstBorder, boolean lastBorder) { 372 mContiguous = contiguous; 373 mExpanded = isExpanded; 374 mFirstBorder = firstBorder; 375 mLastBorder = lastBorder; 376 } 377 378 @Override 379 public int getType() { 380 return VIEW_TYPE_BORDER; 381 } 382 383 @Override 384 public View createView(Context context, LayoutInflater inflater, ViewGroup parent) { 385 return inflater.inflate(R.layout.card_border, parent, false); 386 } 387 388 @Override 389 public void bindView(View v, boolean measureOnly) { 390 final BorderView border = (BorderView) v; 391 border.bind(this, measureOnly); 392 } 393 394 @Override 395 public boolean isContiguous() { 396 return mContiguous; 397 } 398 399 @Override 400 public boolean isExpanded() { 401 return mExpanded; 402 } 403 404 public void setExpanded(boolean isExpanded) { 405 mExpanded = isExpanded; 406 } 407 408 @Override 409 public boolean canPushSnapHeader() { 410 return false; 411 } 412 413 public boolean isFirstBorder() { 414 return mFirstBorder; 415 } 416 417 public boolean isLastBorder() { 418 return mLastBorder; 419 } 420 421 public void setIsLastBorder(boolean isLastBorder) { 422 mLastBorder = isLastBorder; 423 } 424 425 public ConversationViewAdapter getAdapter() { 426 return ConversationViewAdapter.this; 427 } 428 429 @Override 430 public void rebindView(View view) { 431 bindView(view, false); 432 } 433 } 434 435 public ConversationViewAdapter(ControllableActivity controllableActivity, 436 ConversationAccountController accountController, 437 LoaderManager loaderManager, 438 MessageHeaderViewCallbacks messageCallbacks, 439 ContactInfoSource contactInfoSource, 440 ConversationViewHeaderCallbacks convCallbacks, 441 SuperCollapsedBlock.OnClickListener scbListener, Map<String, Address> addressCache, 442 FormattedDateBuilder dateBuilder) { 443 mContext = controllableActivity.getActivityContext(); 444 mDateBuilder = dateBuilder; 445 mAccountController = accountController; 446 mLoaderManager = loaderManager; 447 mFragmentManager = controllableActivity.getFragmentManager(); 448 mMessageCallbacks = messageCallbacks; 449 mContactInfoSource = contactInfoSource; 450 mConversationCallbacks = convCallbacks; 451 mSuperCollapsedListener = scbListener; 452 mAddressCache = addressCache; 453 mInflater = LayoutInflater.from(mContext); 454 455 mItems = Lists.newArrayList(); 456 mMatcher = controllableActivity.getAccountController().getVeiledAddressMatcher(); 457 } 458 459 @Override 460 public int getCount() { 461 return mItems.size(); 462 } 463 464 @Override 465 public int getItemViewType(int position) { 466 return mItems.get(position).getType(); 467 } 468 469 @Override 470 public int getViewTypeCount() { 471 return VIEW_TYPE_COUNT; 472 } 473 474 @Override 475 public ConversationOverlayItem getItem(int position) { 476 return mItems.get(position); 477 } 478 479 @Override 480 public long getItemId(int position) { 481 return position; // TODO: ensure this works well enough 482 } 483 484 @Override 485 public View getView(int position, View convertView, ViewGroup parent) { 486 return getView(getItem(position), convertView, parent, false /* measureOnly */); 487 } 488 489 public View getView(ConversationOverlayItem item, View convertView, ViewGroup parent, 490 boolean measureOnly) { 491 final View v; 492 493 if (convertView == null) { 494 v = item.createView(mContext, mInflater, parent); 495 } else { 496 v = convertView; 497 } 498 item.bindView(v, measureOnly); 499 500 return v; 501 } 502 503 public FormattedDateBuilder getDateBuilder() { 504 return mDateBuilder; 505 } 506 507 public int addItem(ConversationOverlayItem item) { 508 final int pos = mItems.size(); 509 item.setPosition(pos); 510 mItems.add(item); 511 return pos; 512 } 513 514 public void clear() { 515 mItems.clear(); 516 notifyDataSetChanged(); 517 } 518 519 public int addConversationHeader(Conversation conv) { 520 return addItem(new ConversationHeaderItem(conv)); 521 } 522 523 public int addMessageHeader(ConversationMessage msg, boolean expanded, boolean showImages) { 524 return addItem(new MessageHeaderItem(this, mDateBuilder, msg, expanded, showImages)); 525 } 526 527 public int addMessageFooter(MessageHeaderItem headerItem) { 528 return addItem(new MessageFooterItem(headerItem)); 529 } 530 531 public static MessageHeaderItem newMessageHeaderItem(ConversationViewAdapter adapter, 532 FormattedDateBuilder dateBuilder, ConversationMessage message, 533 boolean expanded, boolean showImages) { 534 return new MessageHeaderItem(adapter, dateBuilder, message, expanded, showImages); 535 } 536 537 public MessageFooterItem newMessageFooterItem(MessageHeaderItem headerItem) { 538 return new MessageFooterItem(headerItem); 539 } 540 541 public int addSuperCollapsedBlock(int start, int end) { 542 return addItem(new SuperCollapsedBlockItem(start, end)); 543 } 544 545 public int addBorder( 546 boolean contiguous, boolean expanded, boolean firstBorder, boolean lastBorder) { 547 return addItem(new BorderItem(contiguous, expanded, firstBorder, lastBorder)); 548 } 549 550 public BorderItem newBorderItem(boolean contiguous, boolean expanded) { 551 return new BorderItem( 552 contiguous, expanded, false /* firstBorder */, false /* lastBorder */); 553 } 554 555 public void replaceSuperCollapsedBlock(SuperCollapsedBlockItem blockToRemove, 556 Collection<ConversationOverlayItem> replacements) { 557 final int pos = mItems.indexOf(blockToRemove); 558 if (pos == -1) { 559 return; 560 } 561 562 mItems.remove(pos); 563 mItems.addAll(pos, replacements); 564 565 // update position for all items 566 for (int i = 0, size = mItems.size(); i < size; i++) { 567 mItems.get(i).setPosition(i); 568 } 569 } 570 571 public void updateItemsForMessage(ConversationMessage message, 572 List<Integer> affectedPositions) { 573 for (int i = 0, len = mItems.size(); i < len; i++) { 574 final ConversationOverlayItem item = mItems.get(i); 575 if (item.belongsToMessage(message)) { 576 item.setMessage(message); 577 affectedPositions.add(i); 578 } 579 } 580 } 581} 582