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