• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.view.Menu;
30 import android.view.MenuInflater;
31 import android.view.MenuItem;
32 
33 import com.android.emailcommon.mail.Address;
34 import com.android.mail.R;
35 import com.android.mail.analytics.Analytics;
36 import com.android.mail.browse.ConversationAccountController;
37 import com.android.mail.browse.ConversationMessage;
38 import com.android.mail.browse.ConversationViewHeader.ConversationViewHeaderCallbacks;
39 import com.android.mail.browse.MessageCursor;
40 import com.android.mail.browse.MessageCursor.ConversationController;
41 import com.android.mail.content.ObjectCursor;
42 import com.android.mail.content.ObjectCursorLoader;
43 import com.android.mail.providers.Account;
44 import com.android.mail.providers.AccountObserver;
45 import com.android.mail.providers.Conversation;
46 import com.android.mail.providers.Folder;
47 import com.android.mail.providers.ListParams;
48 import com.android.mail.providers.Settings;
49 import com.android.mail.providers.UIProvider;
50 import com.android.mail.providers.UIProvider.CursorStatus;
51 import com.android.mail.utils.LogTag;
52 import com.android.mail.utils.LogUtils;
53 import com.android.mail.utils.Utils;
54 
55 import java.util.Arrays;
56 import java.util.Collections;
57 import java.util.HashMap;
58 import java.util.Map;
59 
60 public 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 
makeBasicArgs(Account account)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      */
AbstractConversationViewFragment()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      */
markUnread()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      */
onMessageCursorLoadFinished( Loader<ObjectCursor<ConversationMessage>> loader, MessageCursor newCursor, MessageCursor oldCursor)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
onConversationViewHeaderHeightChange(int newHeight)178     public abstract void onConversationViewHeaderHeightChange(int newHeight);
179 
onUserVisibleHintChanged()180     public abstract void onUserVisibleHintChanged();
181 
182     /**
183      * Subclasses must override this.
184      */
onAccountChanged(Account newAccount, Account oldAccount)185     protected abstract void onAccountChanged(Account newAccount, Account oldAccount);
186 
187     @Override
onCreate(Bundle savedState)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      */
parseArguments()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      */
setBaseUri()226     protected void setBaseUri() {
227         mBaseUri = buildBaseUri(getContext(), mAccount, mConversation);
228     }
229 
buildBaseUri(Context context, Account account, Conversation conversation)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
toString()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
onActivityCreated(Bundle savedInstanceState)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
getListController()266     public ConversationUpdater getListController() {
267         final ControllableActivity activity = (ControllableActivity) getActivity();
268         return activity != null ? activity.getConversationUpdater() : null;
269     }
270 
getContext()271     public Context getContext() {
272         return mContext;
273     }
274 
275     @Override
getConversation()276     public Conversation getConversation() {
277         return mConversation;
278     }
279 
280     @Override
getMessageCursor()281     public MessageCursor getMessageCursor() {
282         return mCursor;
283     }
284 
getHandler()285     public Handler getHandler() {
286         return mHandler;
287     }
288 
getMessageLoaderCallbacks()289     public MessageLoaderCallbacks getMessageLoaderCallbacks() {
290         return mMessageLoaderCallbacks;
291     }
292 
getContactInfoSource()293     public ContactLoaderCallbacks getContactInfoSource() {
294         if (mContactLoaderCallbacks == null) {
295             mContactLoaderCallbacks = mActivity.getContactLoaderCallbacks();
296         }
297         return mContactLoaderCallbacks;
298     }
299 
300     @Override
getAccount()301     public Account getAccount() {
302         return mAccount;
303     }
304 
305     @Override
onCreateOptionsMenu(Menu menu, MenuInflater inflater)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
onOptionsItemSelected(MenuItem item)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
onPrepareOptionsMenu(Menu menu)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 
supportsMessageTransforms()364     abstract boolean supportsMessageTransforms();
365 
366     // BEGIN conversation header callbacks
367     @Override
onFoldersClicked()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
onStart()378     public void onStart() {
379         super.onStart();
380 
381         Analytics.getInstance().sendView(getClass().getName());
382     }
383 
384     @Override
onSaveInstanceState(Bundle outState)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
onDestroyView()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      */
setExtraUserVisibleHint(boolean isVisibleToUser)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 
isUserVisible()421     public boolean isUserVisible() {
422         return mUserVisible;
423     }
424 
timerMark(String msg)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
onCreateLoader(int id, Bundle args)435         public Loader<ObjectCursor<ConversationMessage>> onCreateLoader(int id, Bundle args) {
436             return new MessageLoader(mActivity.getActivityContext(), mConversation.messageListUri);
437         }
438 
439         @Override
onLoadFinished(Loader<ObjectCursor<ConversationMessage>> loader, ObjectCursor<ConversationMessage> data)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
onLoaderReset(Loader<ObjectCursor<ConversationMessage>> loader)495         public void onLoaderReset(Loader<ObjectCursor<ConversationMessage>>  loader) {
496             mCursor = null;
497         }
498 
499     }
500 
onError()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 
popOut()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      */
getCurrentFolderTypeDesc()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 
logConversationView()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 
onConversationSeen()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 
getNewViewState()605     protected ConversationViewState getNewViewState() {
606         return new ConversationViewState();
607     }
608 
609     private static class MessageLoader extends ObjectCursorLoader<ConversationMessage> {
610         private boolean mDeliveredFirstResults = false;
611 
MessageLoader(Context c, Uri messageListUri)612         public MessageLoader(Context c, Uri messageListUri) {
613             super(c, messageListUri, UIProvider.MESSAGE_PROJECTION, ConversationMessage.FACTORY);
614         }
615 
616         @Override
deliverResult(ObjectCursor<ConversationMessage> result)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
getObjectCursor(Cursor inner)642         protected ObjectCursor<ConversationMessage> getObjectCursor(Cursor inner) {
643             return new MessageCursor(inner);
644         }
645     }
646 
onConversationUpdated(Conversation conversation)647     public abstract void onConversationUpdated(Conversation conversation);
648 
onDetachedModeEntered()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      */
onConversationTransformed()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      */
showUntransformedConversation()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      */
shouldApplyTransforms()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      */
shouldShowPrintInOverflow()702     protected abstract boolean shouldShowPrintInOverflow();
703 
704     /**
705      * Prints all messages in the conversation.
706      */
printConversation()707     protected abstract void printConversation();
708 
shouldAlwaysShowImages()709     public boolean shouldAlwaysShowImages() {
710         return (mAccount != null) && (mAccount.settings.showImages == Settings.ShowImages.ALWAYS);
711     }
712 }
713