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