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.ui; 19 20 import android.app.Activity; 21 import android.app.Fragment; 22 import android.app.LoaderManager; 23 import android.content.Context; 24 import android.content.Loader; 25 import android.database.Cursor; 26 import android.net.Uri; 27 import android.os.Bundle; 28 import android.os.Handler; 29 import android.support.annotation.Nullable; 30 import android.view.Menu; 31 import android.view.MenuInflater; 32 import android.view.MenuItem; 33 34 import com.android.emailcommon.mail.Address; 35 import com.android.mail.R; 36 import com.android.mail.analytics.Analytics; 37 import com.android.mail.browse.ConversationAccountController; 38 import com.android.mail.browse.ConversationMessage; 39 import com.android.mail.browse.ConversationViewHeader.ConversationViewHeaderCallbacks; 40 import com.android.mail.browse.MessageCursor; 41 import com.android.mail.browse.MessageCursor.ConversationController; 42 import com.android.mail.content.ObjectCursor; 43 import com.android.mail.content.ObjectCursorLoader; 44 import com.android.mail.providers.Account; 45 import com.android.mail.providers.AccountObserver; 46 import com.android.mail.providers.Conversation; 47 import com.android.mail.providers.Folder; 48 import com.android.mail.providers.ListParams; 49 import com.android.mail.providers.Settings; 50 import com.android.mail.providers.UIProvider; 51 import com.android.mail.providers.UIProvider.CursorStatus; 52 import com.android.mail.utils.LogTag; 53 import com.android.mail.utils.LogUtils; 54 import com.android.mail.utils.Utils; 55 56 import java.util.Arrays; 57 import java.util.Collections; 58 import java.util.HashMap; 59 import java.util.Map; 60 61 public abstract class AbstractConversationViewFragment extends Fragment implements 62 ConversationController, ConversationAccountController, 63 ConversationViewHeaderCallbacks { 64 65 protected static final String ARG_ACCOUNT = "account"; 66 public static final String ARG_CONVERSATION = "conversation"; 67 private static final String LOG_TAG = LogTag.getLogTag(); 68 protected static final int MESSAGE_LOADER = 0; 69 protected static final int CONTACT_LOADER = 1; 70 public static final int ATTACHMENT_OPTION1_LOADER = 2; 71 protected ControllableActivity mActivity; 72 private final MessageLoaderCallbacks mMessageLoaderCallbacks = new MessageLoaderCallbacks(); 73 private ContactLoaderCallbacks mContactLoaderCallbacks; 74 private MenuItem mChangeFoldersMenuItem; 75 protected Conversation mConversation; 76 protected String mBaseUri; 77 protected Account mAccount; 78 79 /** 80 * Must be instantiated in a derived class's onCreate. 81 */ 82 protected AbstractConversationWebViewClient mWebViewClient; 83 84 /** 85 * Cache of email address strings to parsed Address objects. 86 * <p> 87 * Remember to synchronize on the map when reading or writing to this cache, because some 88 * instances use it off the UI thread (e.g. from WebView). 89 */ 90 protected final Map<String, Address> mAddressCache = Collections.synchronizedMap( 91 new HashMap<String, Address>()); 92 private MessageCursor mCursor; 93 private Context mContext; 94 /** 95 * A backwards-compatible version of {{@link #getUserVisibleHint()}. Like the framework flag, 96 * this flag is saved and restored. 97 */ 98 private boolean mUserVisible; 99 100 private final Handler mHandler = new Handler(); 101 /** True if we want to avoid marking the conversation as viewed and read. */ 102 private boolean mSuppressMarkingViewed; 103 /** 104 * Parcelable state of the conversation view. Can safely be used without null checking any time 105 * after {@link #onCreate(Bundle)}. 106 */ 107 protected ConversationViewState mViewState; 108 109 private boolean mIsDetached; 110 111 private boolean mHasConversationBeenTransformed; 112 private boolean mHasConversationTransformBeenReverted; 113 114 protected boolean mConversationSeen = false; 115 116 private final AccountObserver mAccountObserver = new AccountObserver() { 117 @Override 118 public void onChanged(Account newAccount) { 119 final Account oldAccount = mAccount; 120 mAccount = newAccount; 121 mWebViewClient.setAccount(mAccount); 122 onAccountChanged(newAccount, oldAccount); 123 } 124 }; 125 126 private static final String BUNDLE_VIEW_STATE = 127 AbstractConversationViewFragment.class.getName() + "viewstate"; 128 /** 129 * We save the user visible flag so the various transitions that occur during rotation do not 130 * cause unnecessary visibility change. 131 */ 132 private static final String BUNDLE_USER_VISIBLE = 133 AbstractConversationViewFragment.class.getName() + "uservisible"; 134 135 private static final String BUNDLE_DETACHED = 136 AbstractConversationViewFragment.class.getName() + "detached"; 137 138 private static final String BUNDLE_KEY_HAS_CONVERSATION_BEEN_TRANSFORMED = 139 AbstractConversationViewFragment.class.getName() + "conversationtransformed"; 140 private static final String BUNDLE_KEY_HAS_CONVERSATION_BEEN_REVERTED = 141 AbstractConversationViewFragment.class.getName() + "conversationreverted"; 142 143 public static Bundle makeBasicArgs(Account account) { 144 Bundle args = new Bundle(); 145 args.putParcelable(ARG_ACCOUNT, account); 146 return args; 147 } 148 149 /** 150 * Constructor needs to be public to handle orientation changes and activity 151 * lifecycle events. 152 */ 153 public AbstractConversationViewFragment() { 154 super(); 155 } 156 157 /** 158 * Subclasses must override, since this depends on how many messages are 159 * shown in the conversation view. 160 */ 161 protected void markUnread() { 162 // Do not automatically mark this conversation viewed and read. 163 mSuppressMarkingViewed = true; 164 } 165 166 /** 167 * Marks a conversation either 'seen' (force=false), as in when the conversation is made visible 168 * and should be marked read, or 'read' (force=true), as in when the action bar menu item to 169 * mark this conversation read is selected. 170 * 171 * @param force true to force marking it read, false to allow peek mode to prevent it 172 */ 173 private final void markRead(boolean force) { 174 final ControllableActivity activity = (ControllableActivity) getActivity(); 175 if (activity == null) { 176 return; 177 } 178 179 // mark viewed/read if not previously marked viewed by this conversation view, 180 // or if unread messages still exist in the message list cursor 181 // we don't want to keep marking viewed on rotation or restore 182 // but we do want future re-renders to mark read (e.g. "New message from X" case) 183 final MessageCursor cursor = getMessageCursor(); 184 LogUtils.d(LOG_TAG, "onConversationSeen() - mConversation.isViewed() = %b, " 185 + "cursor null = %b, cursor.isConversationRead() = %b", 186 mConversation.isViewed(), cursor == null, 187 cursor != null && cursor.isConversationRead()); 188 if (!mConversation.isViewed() || (cursor != null && !cursor.isConversationRead())) { 189 // Mark the conversation read no matter what if force=true. 190 // else only mark it seen if appropriate (2-pane peek=true doesn't mark things seen) 191 final boolean convMarkedRead; 192 if (force) { 193 activity.getConversationUpdater() 194 .markConversationsRead(Arrays.asList(mConversation), true /* read */, 195 true /* viewed */); 196 convMarkedRead = true; 197 } else { 198 convMarkedRead = activity.getConversationUpdater() 199 .markConversationSeen(mConversation); 200 } 201 202 // and update the Message objects in the cursor so the next time a cursor update 203 // happens with these messages marked read, we know to ignore it 204 if (convMarkedRead && cursor != null && !cursor.isClosed()) { 205 cursor.markMessagesRead(); 206 } 207 } 208 } 209 210 /** 211 * Subclasses must override this, since they may want to display a single or 212 * many messages related to this conversation. 213 */ 214 protected abstract void onMessageCursorLoadFinished( 215 Loader<ObjectCursor<ConversationMessage>> loader, 216 MessageCursor newCursor, MessageCursor oldCursor); 217 218 /** 219 * Subclasses must override this, since they may want to display a single or 220 * many messages related to this conversation. 221 */ 222 @Override 223 public abstract void onConversationViewHeaderHeightChange(int newHeight); 224 225 public abstract void onUserVisibleHintChanged(); 226 227 /** 228 * Subclasses must override this. 229 */ 230 protected abstract void onAccountChanged(Account newAccount, Account oldAccount); 231 232 @Override 233 public void onCreate(Bundle savedState) { 234 super.onCreate(savedState); 235 236 parseArguments(); 237 setBaseUri(); 238 239 LogUtils.d(LOG_TAG, "onCreate in ConversationViewFragment (this=%s)", this); 240 // Not really, we just want to get a crack to store a reference to the change_folder item 241 setHasOptionsMenu(true); 242 243 if (savedState != null) { 244 mViewState = savedState.getParcelable(BUNDLE_VIEW_STATE); 245 mUserVisible = savedState.getBoolean(BUNDLE_USER_VISIBLE); 246 mIsDetached = savedState.getBoolean(BUNDLE_DETACHED, false); 247 mHasConversationBeenTransformed = 248 savedState.getBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_TRANSFORMED, false); 249 mHasConversationTransformBeenReverted = 250 savedState.getBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_REVERTED, false); 251 } else { 252 mViewState = getNewViewState(); 253 mHasConversationBeenTransformed = false; 254 mHasConversationTransformBeenReverted = false; 255 } 256 } 257 258 /** 259 * Can be overridden in case a subclass needs to get additional arguments. 260 */ 261 protected void parseArguments() { 262 final Bundle args = getArguments(); 263 mAccount = args.getParcelable(ARG_ACCOUNT); 264 mConversation = args.getParcelable(ARG_CONVERSATION); 265 } 266 267 /** 268 * Can be overridden in case a subclass needs a different uri format 269 * (such as one that does not rely on account and/or conversation. 270 */ 271 protected void setBaseUri() { 272 mBaseUri = buildBaseUri(getContext(), mAccount, mConversation); 273 } 274 275 public static String buildBaseUri(Context context, Account account, Conversation conversation) { 276 // Since the uri specified in the conversation base uri may not be unique, we specify a 277 // base uri that us guaranteed to be unique for this conversation. 278 return "x-thread://" + account.getAccountId().hashCode() + "/" + conversation.id; 279 } 280 281 @Override 282 public String toString() { 283 // log extra info at DEBUG level or finer 284 final String s = super.toString(); 285 if (!LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG) || mConversation == null) { 286 return s; 287 } 288 return "(" + s + " conv=" + mConversation + ")"; 289 } 290 291 @Override 292 public void onActivityCreated(Bundle savedInstanceState) { 293 super.onActivityCreated(savedInstanceState); 294 final Activity activity = getActivity(); 295 if (!(activity instanceof ControllableActivity)) { 296 LogUtils.wtf(LOG_TAG, "ConversationViewFragment expects only a ControllableActivity to" 297 + "create it. Cannot proceed."); 298 } 299 if (activity == null || activity.isFinishing()) { 300 // Activity is finishing, just bail. 301 return; 302 } 303 mActivity = (ControllableActivity) activity; 304 mContext = activity.getApplicationContext(); 305 mWebViewClient.setActivity(activity); 306 mAccount = mAccountObserver.initialize(mActivity.getAccountController()); 307 mWebViewClient.setAccount(mAccount); 308 } 309 310 @Override 311 public ConversationUpdater getListController() { 312 final ControllableActivity activity = (ControllableActivity) getActivity(); 313 return activity != null ? activity.getConversationUpdater() : null; 314 } 315 316 public Context getContext() { 317 return mContext; 318 } 319 320 @Override 321 public Conversation getConversation() { 322 return mConversation; 323 } 324 325 @Override 326 public @Nullable MessageCursor getMessageCursor() { 327 return mCursor; 328 } 329 330 public Handler getHandler() { 331 return mHandler; 332 } 333 334 public MessageLoaderCallbacks getMessageLoaderCallbacks() { 335 return mMessageLoaderCallbacks; 336 } 337 338 public ContactLoaderCallbacks getContactInfoSource() { 339 if (mContactLoaderCallbacks == null) { 340 mContactLoaderCallbacks = mActivity.getContactLoaderCallbacks(); 341 } 342 return mContactLoaderCallbacks; 343 } 344 345 @Override 346 public Account getAccount() { 347 return mAccount; 348 } 349 350 @Override 351 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 352 super.onCreateOptionsMenu(menu, inflater); 353 mChangeFoldersMenuItem = menu.findItem(R.id.change_folders); 354 } 355 356 @Override 357 public boolean onOptionsItemSelected(MenuItem item) { 358 if (!isUserVisible()) { 359 // Unclear how this is happening. Current theory is that this fragment was scheduled 360 // to be removed, but the remove transaction failed. When the Activity is later 361 // restored, the FragmentManager restores this fragment, but Fragment.mMenuVisible is 362 // stuck at its initial value (true), which makes this zombie fragment eligible for 363 // menu item clicks. 364 // 365 // Work around this by relying on the (properly restored) extra user visible hint. 366 LogUtils.e(LOG_TAG, 367 "ACVF ignoring onOptionsItemSelected b/c userVisibleHint is false. f=%s", this); 368 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) { 369 LogUtils.e(LOG_TAG, "%s", Utils.dumpFragment(this)); 370 } 371 return false; 372 } 373 374 boolean handled = true; 375 final int itemId = item.getItemId(); 376 if (itemId == R.id.inside_conversation_unread || itemId == R.id.toggle_read_unread) { 377 markUnread(); 378 } else if (itemId == R.id.read) { 379 markRead(true /* force */); 380 mActivity.supportInvalidateOptionsMenu(); 381 } else if (itemId == R.id.show_original) { 382 showUntransformedConversation(); 383 } else if (itemId == R.id.print_all) { 384 printConversation(); 385 } else if (itemId == R.id.reply) { 386 handleReply(); 387 } else if (itemId == R.id.reply_all) { 388 handleReplyAll(); 389 } else { 390 handled = false; 391 } 392 return handled; 393 } 394 395 @Override 396 public void onPrepareOptionsMenu(Menu menu) { 397 // Only show option if we support message transforms and message has been transformed. 398 Utils.setMenuItemPresent(menu, R.id.show_original, supportsMessageTransforms() && 399 mHasConversationBeenTransformed && !mHasConversationTransformBeenReverted); 400 401 final MenuItem printMenuItem = menu.findItem(R.id.print_all); 402 if (printMenuItem != null) { 403 // compute the visibility of the print menu item 404 printMenuItem.setVisible(Utils.isRunningKitkatOrLater() && shouldShowPrintInOverflow()); 405 406 // compute the text displayed on the print menu item 407 if (mConversation.getNumMessages() == 1) { 408 printMenuItem.setTitle(R.string.print); 409 } else { 410 printMenuItem.setTitle(R.string.print_all); 411 } 412 } 413 } 414 415 abstract boolean supportsMessageTransforms(); 416 417 // BEGIN conversation header callbacks 418 @Override 419 public void onFoldersClicked() { 420 if (mChangeFoldersMenuItem == null) { 421 LogUtils.e(LOG_TAG, "unable to open 'change folders' dialog for a conversation"); 422 return; 423 } 424 mActivity.onOptionsItemSelected(mChangeFoldersMenuItem); 425 } 426 // END conversation header callbacks 427 428 @Override 429 public void onStart() { 430 super.onStart(); 431 432 Analytics.getInstance().sendView(getClass().getName()); 433 } 434 435 @Override 436 public void onSaveInstanceState(Bundle outState) { 437 if (mViewState != null) { 438 outState.putParcelable(BUNDLE_VIEW_STATE, mViewState); 439 } 440 outState.putBoolean(BUNDLE_USER_VISIBLE, mUserVisible); 441 outState.putBoolean(BUNDLE_DETACHED, mIsDetached); 442 outState.putBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_TRANSFORMED, 443 mHasConversationBeenTransformed); 444 outState.putBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_REVERTED, 445 mHasConversationTransformBeenReverted); 446 } 447 448 @Override 449 public void onDestroyView() { 450 super.onDestroyView(); 451 mAccountObserver.unregisterAndDestroy(); 452 } 453 454 /** 455 * {@link #setUserVisibleHint(boolean)} only works on API >= 15, so implement our own for 456 * reliability on older platforms. 457 */ 458 public void setExtraUserVisibleHint(boolean isVisibleToUser) { 459 LogUtils.v(LOG_TAG, "in CVF.setHint, val=%s (%s)", isVisibleToUser, this); 460 if (mUserVisible != isVisibleToUser) { 461 mUserVisible = isVisibleToUser; 462 MessageCursor cursor = getMessageCursor(); 463 if (mUserVisible && (cursor != null && cursor.isLoaded() && cursor.getCount() == 0)) { 464 // Pop back to conversation list and show error. 465 onError(); 466 return; 467 } 468 onUserVisibleHintChanged(); 469 } 470 } 471 472 public boolean isUserVisible() { 473 return mUserVisible; 474 } 475 476 protected void timerMark(String msg) { 477 if (isUserVisible()) { 478 Utils.sConvLoadTimer.mark(msg); 479 } 480 } 481 482 private class MessageLoaderCallbacks 483 implements LoaderManager.LoaderCallbacks<ObjectCursor<ConversationMessage>> { 484 485 @Override 486 public Loader<ObjectCursor<ConversationMessage>> onCreateLoader(int id, Bundle args) { 487 return new MessageLoader(mActivity.getActivityContext(), mConversation.messageListUri); 488 } 489 490 @Override 491 public void onLoadFinished(Loader<ObjectCursor<ConversationMessage>> loader, 492 ObjectCursor<ConversationMessage> data) { 493 // ignore truly duplicate results 494 // this can happen when restoring after rotation 495 if (mCursor == data) { 496 return; 497 } else { 498 final MessageCursor messageCursor = (MessageCursor) data; 499 500 // bind the cursor to this fragment so it can access to the current list controller 501 messageCursor.setController(AbstractConversationViewFragment.this); 502 503 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) { 504 LogUtils.d(LOG_TAG, "LOADED CONVERSATION= %s", messageCursor.getDebugDump()); 505 } 506 507 // We have no messages: exit conversation view. 508 if (messageCursor.getCount() == 0 509 && (!CursorStatus.isWaitingForResults(messageCursor.getStatus()) 510 || mIsDetached)) { 511 if (mUserVisible) { 512 onError(); 513 } else { 514 // we expect that the pager adapter will remove this 515 // conversation fragment on its own due to a separate 516 // conversation cursor update (we might get here if the 517 // message list update fires first. nothing to do 518 // because we expect to be torn down soon.) 519 LogUtils.i(LOG_TAG, "CVF: offscreen conv has no messages, ignoring update" 520 + " in anticipation of conv cursor update. c=%s", 521 mConversation.uri); 522 } 523 // existing mCursor will imminently be closed, must stop referencing it 524 // since we expect to be kicked out soon, it doesn't matter what mCursor 525 // becomes 526 mCursor = null; 527 return; 528 } 529 530 // ignore cursors that are still loading results 531 if (!messageCursor.isLoaded()) { 532 // existing mCursor will imminently be closed, must stop referencing it 533 // in this case, the new cursor is also no good, and since don't expect to get 534 // here except in initial load situations, it's safest to just ensure the 535 // reference is null 536 mCursor = null; 537 return; 538 } 539 final MessageCursor oldCursor = mCursor; 540 mCursor = messageCursor; 541 onMessageCursorLoadFinished(loader, mCursor, oldCursor); 542 } 543 } 544 545 @Override 546 public void onLoaderReset(Loader<ObjectCursor<ConversationMessage>> loader) { 547 mCursor = null; 548 } 549 550 } 551 552 private void onError() { 553 // need to exit this view- conversation may have been 554 // deleted, or for whatever reason is now invalid (e.g. 555 // discard single draft) 556 // 557 // N.B. this may involve a fragment transaction, which 558 // FragmentManager will refuse to execute directly 559 // within onLoadFinished. Make sure the controller knows. 560 LogUtils.i(LOG_TAG, "CVF: visible conv has no messages, exiting conv mode"); 561 // TODO(mindyp): handle ERROR status by showing an error 562 // message to the user that there are no messages in 563 // this conversation 564 popOut(); 565 } 566 567 private void popOut() { 568 mHandler.post(new FragmentRunnable("popOut", this) { 569 @Override 570 public void go() { 571 if (mActivity != null) { 572 mActivity.getListHandler() 573 .onConversationSelected(null, true /* inLoaderCallbacks */); 574 } 575 } 576 }); 577 } 578 579 /** 580 * @see Folder#getTypeDescription() 581 */ 582 protected String getCurrentFolderTypeDesc() { 583 final Folder currFolder; 584 if (mActivity != null) { 585 currFolder = mActivity.getFolderController().getFolder(); 586 } else { 587 currFolder = null; 588 } 589 final String folderStr; 590 if (currFolder != null) { 591 folderStr = currFolder.getTypeDescription(); 592 } else { 593 folderStr = "unknown_folder"; 594 } 595 return folderStr; 596 } 597 598 private void logConversationView() { 599 final String folderStr = getCurrentFolderTypeDesc(); 600 Analytics.getInstance().sendEvent("view_conversation", folderStr, 601 mConversation.isRemote ? "unsynced" : "synced", mConversation.getNumMessages()); 602 } 603 604 protected final void onConversationSeen() { 605 LogUtils.d(LOG_TAG, "AbstractConversationViewFragment#onConversationSeen()"); 606 607 // Ignore unsafe calls made after a fragment is detached from an activity 608 final ControllableActivity activity = (ControllableActivity) getActivity(); 609 if (activity == null) { 610 LogUtils.w(LOG_TAG, "ignoring onConversationSeen for conv=%s", mConversation.id); 611 return; 612 } 613 614 // this method is called 2x on rotation; debounce this a bit so as not to 615 // dramatically skew analytics data too much. Ideally, it should be called zero times 616 // on rotation... 617 if (!mConversationSeen) { 618 logConversationView(); 619 } 620 621 mViewState.setInfoForConversation(mConversation); 622 623 LogUtils.d(LOG_TAG, "onConversationSeen() - mSuppressMarkingViewed = %b", 624 mSuppressMarkingViewed); 625 // In most circumstances we want to mark the conversation as viewed and read, since the 626 // user has read it. However, if the user has already marked the conversation unread, we 627 // do not want a later mark-read operation to undo this. So we check this variable which 628 // is set in #markUnread() which suppresses automatic mark-read. 629 if (!mSuppressMarkingViewed) { 630 markRead(false /* force */); 631 } 632 activity.getListHandler().onConversationSeen(); 633 634 mConversationSeen = true; 635 } 636 637 protected ConversationViewState getNewViewState() { 638 return new ConversationViewState(); 639 } 640 641 private static class MessageLoader extends ObjectCursorLoader<ConversationMessage> { 642 private boolean mDeliveredFirstResults = false; 643 644 public MessageLoader(Context c, Uri messageListUri) { 645 super(c, messageListUri, UIProvider.MESSAGE_PROJECTION, ConversationMessage.FACTORY); 646 } 647 648 @Override 649 public void deliverResult(ObjectCursor<ConversationMessage> result) { 650 // We want to deliver these results, and then we want to make sure 651 // that any subsequent 652 // queries do not hit the network 653 super.deliverResult(result); 654 655 if (!mDeliveredFirstResults) { 656 mDeliveredFirstResults = true; 657 Uri uri = getUri(); 658 659 // Create a ListParams that tells the provider to not hit the 660 // network 661 final ListParams listParams = new ListParams(ListParams.NO_LIMIT, 662 false /* useNetwork */); 663 664 // Build the new uri with this additional parameter 665 uri = uri 666 .buildUpon() 667 .appendQueryParameter(UIProvider.LIST_PARAMS_QUERY_PARAMETER, 668 listParams.serialize()).build(); 669 setUri(uri); 670 } 671 } 672 673 @Override 674 protected ObjectCursor<ConversationMessage> getObjectCursor(Cursor inner) { 675 return new MessageCursor(inner); 676 } 677 } 678 679 public abstract void onConversationUpdated(Conversation conversation); 680 681 public void onDetachedModeEntered() { 682 // If we have no messages, then we have nothing to display, so leave this view. 683 // Otherwise, just set the detached flag. 684 final Cursor messageCursor = getMessageCursor(); 685 686 if (messageCursor == null || messageCursor.getCount() == 0) { 687 popOut(); 688 } else { 689 mIsDetached = true; 690 } 691 } 692 693 /** 694 * Called when the JavaScript reports that it transformed a message. 695 * Sets a flag to true and invalidates the options menu so it will 696 * include the "Revert auto-sizing" menu option. 697 */ 698 public void onConversationTransformed() { 699 mHasConversationBeenTransformed = true; 700 mHandler.post(new FragmentRunnable("invalidateOptionsMenu", this) { 701 @Override 702 public void go() { 703 mActivity.supportInvalidateOptionsMenu(); 704 } 705 }); 706 } 707 708 /** 709 * Called when the "Revert auto-sizing" option is selected. Default 710 * implementation simply sets a value on whether transforms should be 711 * applied. Derived classes should override this class and force a 712 * re-render so that the conversation renders without 713 */ 714 public void showUntransformedConversation() { 715 // must set the value to true so we don't show the options menu item again 716 mHasConversationTransformBeenReverted = true; 717 } 718 719 /** 720 * Returns {@code true} if the conversation should be transformed. {@code false}, otherwise. 721 * @return {@code true} if the conversation should be transformed. {@code false}, otherwise. 722 */ 723 public boolean shouldApplyTransforms() { 724 return (mAccount.enableMessageTransforms > 0) && 725 !mHasConversationTransformBeenReverted; 726 } 727 728 /** 729 * The Print item in the overflow menu of the Conversation view is shown based on the return 730 * from this method. 731 * 732 * @return {@code true} if the conversation can be printed; {@code false} otherwise. 733 */ 734 protected abstract boolean shouldShowPrintInOverflow(); 735 736 /** 737 * Prints all messages in the conversation. 738 */ 739 protected abstract void printConversation(); 740 741 // These methods should perform default reply/replyall action on the last message. 742 protected abstract void handleReply(); 743 protected abstract void handleReplyAll(); 744 745 public boolean shouldAlwaysShowImages() { 746 return (mAccount != null) && (mAccount.settings.showImages == Settings.ShowImages.ALWAYS); 747 } 748 } 749