| /* |
| * Copyright (C) 2012 Google Inc. |
| * Licensed to The Android Open Source Project. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.mail.ui; |
| |
| import android.app.Activity; |
| import android.app.Fragment; |
| import android.app.LoaderManager; |
| import android.content.Context; |
| import android.content.Loader; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.support.annotation.Nullable; |
| import android.view.Menu; |
| import android.view.MenuInflater; |
| import android.view.MenuItem; |
| |
| import com.android.emailcommon.mail.Address; |
| import com.android.mail.R; |
| import com.android.mail.analytics.Analytics; |
| import com.android.mail.browse.ConversationAccountController; |
| import com.android.mail.browse.ConversationMessage; |
| import com.android.mail.browse.ConversationViewHeader.ConversationViewHeaderCallbacks; |
| import com.android.mail.browse.MessageCursor; |
| import com.android.mail.browse.MessageCursor.ConversationController; |
| import com.android.mail.content.ObjectCursor; |
| import com.android.mail.content.ObjectCursorLoader; |
| import com.android.mail.providers.Account; |
| import com.android.mail.providers.AccountObserver; |
| import com.android.mail.providers.Conversation; |
| import com.android.mail.providers.Folder; |
| import com.android.mail.providers.ListParams; |
| import com.android.mail.providers.Settings; |
| import com.android.mail.providers.UIProvider; |
| import com.android.mail.providers.UIProvider.CursorStatus; |
| import com.android.mail.utils.LogTag; |
| import com.android.mail.utils.LogUtils; |
| import com.android.mail.utils.Utils; |
| |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Map; |
| |
| public abstract class AbstractConversationViewFragment extends Fragment implements |
| ConversationController, ConversationAccountController, |
| ConversationViewHeaderCallbacks { |
| |
| protected static final String ARG_ACCOUNT = "account"; |
| public static final String ARG_CONVERSATION = "conversation"; |
| private static final String LOG_TAG = LogTag.getLogTag(); |
| protected static final int MESSAGE_LOADER = 0; |
| protected static final int CONTACT_LOADER = 1; |
| public static final int ATTACHMENT_OPTION1_LOADER = 2; |
| protected ControllableActivity mActivity; |
| private final MessageLoaderCallbacks mMessageLoaderCallbacks = new MessageLoaderCallbacks(); |
| private ContactLoaderCallbacks mContactLoaderCallbacks; |
| private MenuItem mChangeFoldersMenuItem; |
| protected Conversation mConversation; |
| protected String mBaseUri; |
| protected Account mAccount; |
| |
| /** |
| * Must be instantiated in a derived class's onCreate. |
| */ |
| protected AbstractConversationWebViewClient mWebViewClient; |
| |
| /** |
| * Cache of email address strings to parsed Address objects. |
| * <p> |
| * Remember to synchronize on the map when reading or writing to this cache, because some |
| * instances use it off the UI thread (e.g. from WebView). |
| */ |
| protected final Map<String, Address> mAddressCache = Collections.synchronizedMap( |
| new HashMap<String, Address>()); |
| private MessageCursor mCursor; |
| private Context mContext; |
| /** |
| * A backwards-compatible version of {{@link #getUserVisibleHint()}. Like the framework flag, |
| * this flag is saved and restored. |
| */ |
| private boolean mUserVisible; |
| |
| private final Handler mHandler = new Handler(); |
| /** True if we want to avoid marking the conversation as viewed and read. */ |
| private boolean mSuppressMarkingViewed; |
| /** |
| * Parcelable state of the conversation view. Can safely be used without null checking any time |
| * after {@link #onCreate(Bundle)}. |
| */ |
| protected ConversationViewState mViewState; |
| |
| private boolean mIsDetached; |
| |
| private boolean mHasConversationBeenTransformed; |
| private boolean mHasConversationTransformBeenReverted; |
| |
| protected boolean mConversationSeen = false; |
| |
| private final AccountObserver mAccountObserver = new AccountObserver() { |
| @Override |
| public void onChanged(Account newAccount) { |
| final Account oldAccount = mAccount; |
| mAccount = newAccount; |
| mWebViewClient.setAccount(mAccount); |
| onAccountChanged(newAccount, oldAccount); |
| } |
| }; |
| |
| private static final String BUNDLE_VIEW_STATE = |
| AbstractConversationViewFragment.class.getName() + "viewstate"; |
| /** |
| * We save the user visible flag so the various transitions that occur during rotation do not |
| * cause unnecessary visibility change. |
| */ |
| private static final String BUNDLE_USER_VISIBLE = |
| AbstractConversationViewFragment.class.getName() + "uservisible"; |
| |
| private static final String BUNDLE_DETACHED = |
| AbstractConversationViewFragment.class.getName() + "detached"; |
| |
| private static final String BUNDLE_KEY_HAS_CONVERSATION_BEEN_TRANSFORMED = |
| AbstractConversationViewFragment.class.getName() + "conversationtransformed"; |
| private static final String BUNDLE_KEY_HAS_CONVERSATION_BEEN_REVERTED = |
| AbstractConversationViewFragment.class.getName() + "conversationreverted"; |
| |
| public static Bundle makeBasicArgs(Account account) { |
| Bundle args = new Bundle(); |
| args.putParcelable(ARG_ACCOUNT, account); |
| return args; |
| } |
| |
| /** |
| * Constructor needs to be public to handle orientation changes and activity |
| * lifecycle events. |
| */ |
| public AbstractConversationViewFragment() { |
| super(); |
| } |
| |
| /** |
| * Subclasses must override, since this depends on how many messages are |
| * shown in the conversation view. |
| */ |
| protected void markUnread() { |
| // Do not automatically mark this conversation viewed and read. |
| mSuppressMarkingViewed = true; |
| } |
| |
| /** |
| * Marks a conversation either 'seen' (force=false), as in when the conversation is made visible |
| * and should be marked read, or 'read' (force=true), as in when the action bar menu item to |
| * mark this conversation read is selected. |
| * |
| * @param force true to force marking it read, false to allow peek mode to prevent it |
| */ |
| private final void markRead(boolean force) { |
| final ControllableActivity activity = (ControllableActivity) getActivity(); |
| if (activity == null) { |
| return; |
| } |
| |
| // mark viewed/read if not previously marked viewed by this conversation view, |
| // or if unread messages still exist in the message list cursor |
| // we don't want to keep marking viewed on rotation or restore |
| // but we do want future re-renders to mark read (e.g. "New message from X" case) |
| final MessageCursor cursor = getMessageCursor(); |
| LogUtils.d(LOG_TAG, "onConversationSeen() - mConversation.isViewed() = %b, " |
| + "cursor null = %b, cursor.isConversationRead() = %b", |
| mConversation.isViewed(), cursor == null, |
| cursor != null && cursor.isConversationRead()); |
| if (!mConversation.isViewed() || (cursor != null && !cursor.isConversationRead())) { |
| // Mark the conversation read no matter what if force=true. |
| // else only mark it seen if appropriate (2-pane peek=true doesn't mark things seen) |
| final boolean convMarkedRead; |
| if (force) { |
| activity.getConversationUpdater() |
| .markConversationsRead(Arrays.asList(mConversation), true /* read */, |
| true /* viewed */); |
| convMarkedRead = true; |
| } else { |
| convMarkedRead = activity.getConversationUpdater() |
| .markConversationSeen(mConversation); |
| } |
| |
| // and update the Message objects in the cursor so the next time a cursor update |
| // happens with these messages marked read, we know to ignore it |
| if (convMarkedRead && cursor != null && !cursor.isClosed()) { |
| cursor.markMessagesRead(); |
| } |
| } |
| } |
| |
| /** |
| * Subclasses must override this, since they may want to display a single or |
| * many messages related to this conversation. |
| */ |
| protected abstract void onMessageCursorLoadFinished( |
| Loader<ObjectCursor<ConversationMessage>> loader, |
| MessageCursor newCursor, MessageCursor oldCursor); |
| |
| /** |
| * Subclasses must override this, since they may want to display a single or |
| * many messages related to this conversation. |
| */ |
| @Override |
| public abstract void onConversationViewHeaderHeightChange(int newHeight); |
| |
| public abstract void onUserVisibleHintChanged(); |
| |
| /** |
| * Subclasses must override this. |
| */ |
| protected abstract void onAccountChanged(Account newAccount, Account oldAccount); |
| |
| @Override |
| public void onCreate(Bundle savedState) { |
| super.onCreate(savedState); |
| |
| parseArguments(); |
| setBaseUri(); |
| |
| LogUtils.d(LOG_TAG, "onCreate in ConversationViewFragment (this=%s)", this); |
| // Not really, we just want to get a crack to store a reference to the change_folder item |
| setHasOptionsMenu(true); |
| |
| if (savedState != null) { |
| mViewState = savedState.getParcelable(BUNDLE_VIEW_STATE); |
| mUserVisible = savedState.getBoolean(BUNDLE_USER_VISIBLE); |
| mIsDetached = savedState.getBoolean(BUNDLE_DETACHED, false); |
| mHasConversationBeenTransformed = |
| savedState.getBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_TRANSFORMED, false); |
| mHasConversationTransformBeenReverted = |
| savedState.getBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_REVERTED, false); |
| } else { |
| mViewState = getNewViewState(); |
| mHasConversationBeenTransformed = false; |
| mHasConversationTransformBeenReverted = false; |
| } |
| } |
| |
| /** |
| * Can be overridden in case a subclass needs to get additional arguments. |
| */ |
| protected void parseArguments() { |
| final Bundle args = getArguments(); |
| mAccount = args.getParcelable(ARG_ACCOUNT); |
| mConversation = args.getParcelable(ARG_CONVERSATION); |
| } |
| |
| /** |
| * Can be overridden in case a subclass needs a different uri format |
| * (such as one that does not rely on account and/or conversation. |
| */ |
| protected void setBaseUri() { |
| mBaseUri = buildBaseUri(getContext(), mAccount, mConversation); |
| } |
| |
| public static String buildBaseUri(Context context, Account account, Conversation conversation) { |
| // Since the uri specified in the conversation base uri may not be unique, we specify a |
| // base uri that us guaranteed to be unique for this conversation. |
| return "x-thread://" + account.getAccountId().hashCode() + "/" + conversation.id; |
| } |
| |
| @Override |
| public String toString() { |
| // log extra info at DEBUG level or finer |
| final String s = super.toString(); |
| if (!LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG) || mConversation == null) { |
| return s; |
| } |
| return "(" + s + " conv=" + mConversation + ")"; |
| } |
| |
| @Override |
| public void onActivityCreated(Bundle savedInstanceState) { |
| super.onActivityCreated(savedInstanceState); |
| final Activity activity = getActivity(); |
| if (!(activity instanceof ControllableActivity)) { |
| LogUtils.wtf(LOG_TAG, "ConversationViewFragment expects only a ControllableActivity to" |
| + "create it. Cannot proceed."); |
| } |
| if (activity == null || activity.isFinishing()) { |
| // Activity is finishing, just bail. |
| return; |
| } |
| mActivity = (ControllableActivity) activity; |
| mContext = activity.getApplicationContext(); |
| mWebViewClient.setActivity(activity); |
| mAccount = mAccountObserver.initialize(mActivity.getAccountController()); |
| mWebViewClient.setAccount(mAccount); |
| } |
| |
| @Override |
| public ConversationUpdater getListController() { |
| final ControllableActivity activity = (ControllableActivity) getActivity(); |
| return activity != null ? activity.getConversationUpdater() : null; |
| } |
| |
| public Context getContext() { |
| return mContext; |
| } |
| |
| @Override |
| public Conversation getConversation() { |
| return mConversation; |
| } |
| |
| @Override |
| public @Nullable MessageCursor getMessageCursor() { |
| return mCursor; |
| } |
| |
| public Handler getHandler() { |
| return mHandler; |
| } |
| |
| public MessageLoaderCallbacks getMessageLoaderCallbacks() { |
| return mMessageLoaderCallbacks; |
| } |
| |
| public ContactLoaderCallbacks getContactInfoSource() { |
| if (mContactLoaderCallbacks == null) { |
| mContactLoaderCallbacks = mActivity.getContactLoaderCallbacks(); |
| } |
| return mContactLoaderCallbacks; |
| } |
| |
| @Override |
| public Account getAccount() { |
| return mAccount; |
| } |
| |
| @Override |
| public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { |
| super.onCreateOptionsMenu(menu, inflater); |
| mChangeFoldersMenuItem = menu.findItem(R.id.change_folders); |
| } |
| |
| @Override |
| public boolean onOptionsItemSelected(MenuItem item) { |
| if (!isUserVisible()) { |
| // Unclear how this is happening. Current theory is that this fragment was scheduled |
| // to be removed, but the remove transaction failed. When the Activity is later |
| // restored, the FragmentManager restores this fragment, but Fragment.mMenuVisible is |
| // stuck at its initial value (true), which makes this zombie fragment eligible for |
| // menu item clicks. |
| // |
| // Work around this by relying on the (properly restored) extra user visible hint. |
| LogUtils.e(LOG_TAG, |
| "ACVF ignoring onOptionsItemSelected b/c userVisibleHint is false. f=%s", this); |
| if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) { |
| LogUtils.e(LOG_TAG, "%s", Utils.dumpFragment(this)); |
| } |
| return false; |
| } |
| |
| boolean handled = true; |
| final int itemId = item.getItemId(); |
| if (itemId == R.id.inside_conversation_unread || itemId == R.id.toggle_read_unread) { |
| markUnread(); |
| } else if (itemId == R.id.read) { |
| markRead(true /* force */); |
| mActivity.supportInvalidateOptionsMenu(); |
| } else if (itemId == R.id.show_original) { |
| showUntransformedConversation(); |
| } else if (itemId == R.id.print_all) { |
| printConversation(); |
| } else if (itemId == R.id.reply) { |
| handleReply(); |
| } else if (itemId == R.id.reply_all) { |
| handleReplyAll(); |
| } else { |
| handled = false; |
| } |
| return handled; |
| } |
| |
| @Override |
| public void onPrepareOptionsMenu(Menu menu) { |
| // Only show option if we support message transforms and message has been transformed. |
| Utils.setMenuItemPresent(menu, R.id.show_original, supportsMessageTransforms() && |
| mHasConversationBeenTransformed && !mHasConversationTransformBeenReverted); |
| |
| final MenuItem printMenuItem = menu.findItem(R.id.print_all); |
| if (printMenuItem != null) { |
| // compute the visibility of the print menu item |
| printMenuItem.setVisible(Utils.isRunningKitkatOrLater() && shouldShowPrintInOverflow()); |
| |
| // compute the text displayed on the print menu item |
| if (mConversation.getNumMessages() == 1) { |
| printMenuItem.setTitle(R.string.print); |
| } else { |
| printMenuItem.setTitle(R.string.print_all); |
| } |
| } |
| } |
| |
| abstract boolean supportsMessageTransforms(); |
| |
| // BEGIN conversation header callbacks |
| @Override |
| public void onFoldersClicked() { |
| if (mChangeFoldersMenuItem == null) { |
| LogUtils.e(LOG_TAG, "unable to open 'change folders' dialog for a conversation"); |
| return; |
| } |
| mActivity.onOptionsItemSelected(mChangeFoldersMenuItem); |
| } |
| // END conversation header callbacks |
| |
| @Override |
| public void onStart() { |
| super.onStart(); |
| |
| Analytics.getInstance().sendView(getClass().getName()); |
| } |
| |
| @Override |
| public void onSaveInstanceState(Bundle outState) { |
| if (mViewState != null) { |
| outState.putParcelable(BUNDLE_VIEW_STATE, mViewState); |
| } |
| outState.putBoolean(BUNDLE_USER_VISIBLE, mUserVisible); |
| outState.putBoolean(BUNDLE_DETACHED, mIsDetached); |
| outState.putBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_TRANSFORMED, |
| mHasConversationBeenTransformed); |
| outState.putBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_REVERTED, |
| mHasConversationTransformBeenReverted); |
| } |
| |
| @Override |
| public void onDestroyView() { |
| super.onDestroyView(); |
| mAccountObserver.unregisterAndDestroy(); |
| } |
| |
| /** |
| * {@link #setUserVisibleHint(boolean)} only works on API >= 15, so implement our own for |
| * reliability on older platforms. |
| */ |
| public void setExtraUserVisibleHint(boolean isVisibleToUser) { |
| LogUtils.v(LOG_TAG, "in CVF.setHint, val=%s (%s)", isVisibleToUser, this); |
| if (mUserVisible != isVisibleToUser) { |
| mUserVisible = isVisibleToUser; |
| MessageCursor cursor = getMessageCursor(); |
| if (mUserVisible && (cursor != null && cursor.isLoaded() && cursor.getCount() == 0)) { |
| // Pop back to conversation list and show error. |
| onError(); |
| return; |
| } |
| onUserVisibleHintChanged(); |
| } |
| } |
| |
| public boolean isUserVisible() { |
| return mUserVisible; |
| } |
| |
| protected void timerMark(String msg) { |
| if (isUserVisible()) { |
| Utils.sConvLoadTimer.mark(msg); |
| } |
| } |
| |
| private class MessageLoaderCallbacks |
| implements LoaderManager.LoaderCallbacks<ObjectCursor<ConversationMessage>> { |
| |
| @Override |
| public Loader<ObjectCursor<ConversationMessage>> onCreateLoader(int id, Bundle args) { |
| return new MessageLoader(mActivity.getActivityContext(), mConversation.messageListUri); |
| } |
| |
| @Override |
| public void onLoadFinished(Loader<ObjectCursor<ConversationMessage>> loader, |
| ObjectCursor<ConversationMessage> data) { |
| // ignore truly duplicate results |
| // this can happen when restoring after rotation |
| if (mCursor == data) { |
| return; |
| } else { |
| final MessageCursor messageCursor = (MessageCursor) data; |
| |
| // bind the cursor to this fragment so it can access to the current list controller |
| messageCursor.setController(AbstractConversationViewFragment.this); |
| |
| if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) { |
| LogUtils.d(LOG_TAG, "LOADED CONVERSATION= %s", messageCursor.getDebugDump()); |
| } |
| |
| // We have no messages: exit conversation view. |
| if (messageCursor.getCount() == 0 |
| && (!CursorStatus.isWaitingForResults(messageCursor.getStatus()) |
| || mIsDetached)) { |
| if (mUserVisible) { |
| onError(); |
| } else { |
| // we expect that the pager adapter will remove this |
| // conversation fragment on its own due to a separate |
| // conversation cursor update (we might get here if the |
| // message list update fires first. nothing to do |
| // because we expect to be torn down soon.) |
| LogUtils.i(LOG_TAG, "CVF: offscreen conv has no messages, ignoring update" |
| + " in anticipation of conv cursor update. c=%s", |
| mConversation.uri); |
| } |
| // existing mCursor will imminently be closed, must stop referencing it |
| // since we expect to be kicked out soon, it doesn't matter what mCursor |
| // becomes |
| mCursor = null; |
| return; |
| } |
| |
| // ignore cursors that are still loading results |
| if (!messageCursor.isLoaded()) { |
| // existing mCursor will imminently be closed, must stop referencing it |
| // in this case, the new cursor is also no good, and since don't expect to get |
| // here except in initial load situations, it's safest to just ensure the |
| // reference is null |
| mCursor = null; |
| return; |
| } |
| final MessageCursor oldCursor = mCursor; |
| mCursor = messageCursor; |
| onMessageCursorLoadFinished(loader, mCursor, oldCursor); |
| } |
| } |
| |
| @Override |
| public void onLoaderReset(Loader<ObjectCursor<ConversationMessage>> loader) { |
| mCursor = null; |
| } |
| |
| } |
| |
| private void onError() { |
| // need to exit this view- conversation may have been |
| // deleted, or for whatever reason is now invalid (e.g. |
| // discard single draft) |
| // |
| // N.B. this may involve a fragment transaction, which |
| // FragmentManager will refuse to execute directly |
| // within onLoadFinished. Make sure the controller knows. |
| LogUtils.i(LOG_TAG, "CVF: visible conv has no messages, exiting conv mode"); |
| // TODO(mindyp): handle ERROR status by showing an error |
| // message to the user that there are no messages in |
| // this conversation |
| popOut(); |
| } |
| |
| private void popOut() { |
| mHandler.post(new FragmentRunnable("popOut", this) { |
| @Override |
| public void go() { |
| if (mActivity != null) { |
| mActivity.getListHandler() |
| .onConversationSelected(null, true /* inLoaderCallbacks */); |
| } |
| } |
| }); |
| } |
| |
| /** |
| * @see Folder#getTypeDescription() |
| */ |
| protected String getCurrentFolderTypeDesc() { |
| final Folder currFolder; |
| if (mActivity != null) { |
| currFolder = mActivity.getFolderController().getFolder(); |
| } else { |
| currFolder = null; |
| } |
| final String folderStr; |
| if (currFolder != null) { |
| folderStr = currFolder.getTypeDescription(); |
| } else { |
| folderStr = "unknown_folder"; |
| } |
| return folderStr; |
| } |
| |
| private void logConversationView() { |
| final String folderStr = getCurrentFolderTypeDesc(); |
| Analytics.getInstance().sendEvent("view_conversation", folderStr, |
| mConversation.isRemote ? "unsynced" : "synced", mConversation.getNumMessages()); |
| } |
| |
| protected final void onConversationSeen() { |
| LogUtils.d(LOG_TAG, "AbstractConversationViewFragment#onConversationSeen()"); |
| |
| // Ignore unsafe calls made after a fragment is detached from an activity |
| final ControllableActivity activity = (ControllableActivity) getActivity(); |
| if (activity == null) { |
| LogUtils.w(LOG_TAG, "ignoring onConversationSeen for conv=%s", mConversation.id); |
| return; |
| } |
| |
| // this method is called 2x on rotation; debounce this a bit so as not to |
| // dramatically skew analytics data too much. Ideally, it should be called zero times |
| // on rotation... |
| if (!mConversationSeen) { |
| logConversationView(); |
| } |
| |
| mViewState.setInfoForConversation(mConversation); |
| |
| LogUtils.d(LOG_TAG, "onConversationSeen() - mSuppressMarkingViewed = %b", |
| mSuppressMarkingViewed); |
| // In most circumstances we want to mark the conversation as viewed and read, since the |
| // user has read it. However, if the user has already marked the conversation unread, we |
| // do not want a later mark-read operation to undo this. So we check this variable which |
| // is set in #markUnread() which suppresses automatic mark-read. |
| if (!mSuppressMarkingViewed) { |
| markRead(false /* force */); |
| } |
| activity.getListHandler().onConversationSeen(); |
| |
| mConversationSeen = true; |
| } |
| |
| protected ConversationViewState getNewViewState() { |
| return new ConversationViewState(); |
| } |
| |
| private static class MessageLoader extends ObjectCursorLoader<ConversationMessage> { |
| private boolean mDeliveredFirstResults = false; |
| |
| public MessageLoader(Context c, Uri messageListUri) { |
| super(c, messageListUri, UIProvider.MESSAGE_PROJECTION, ConversationMessage.FACTORY); |
| } |
| |
| @Override |
| public void deliverResult(ObjectCursor<ConversationMessage> result) { |
| // We want to deliver these results, and then we want to make sure |
| // that any subsequent |
| // queries do not hit the network |
| super.deliverResult(result); |
| |
| if (!mDeliveredFirstResults) { |
| mDeliveredFirstResults = true; |
| Uri uri = getUri(); |
| |
| // Create a ListParams that tells the provider to not hit the |
| // network |
| final ListParams listParams = new ListParams(ListParams.NO_LIMIT, |
| false /* useNetwork */); |
| |
| // Build the new uri with this additional parameter |
| uri = uri |
| .buildUpon() |
| .appendQueryParameter(UIProvider.LIST_PARAMS_QUERY_PARAMETER, |
| listParams.serialize()).build(); |
| setUri(uri); |
| } |
| } |
| |
| @Override |
| protected ObjectCursor<ConversationMessage> getObjectCursor(Cursor inner) { |
| return new MessageCursor(inner); |
| } |
| } |
| |
| public abstract void onConversationUpdated(Conversation conversation); |
| |
| public void onDetachedModeEntered() { |
| // If we have no messages, then we have nothing to display, so leave this view. |
| // Otherwise, just set the detached flag. |
| final Cursor messageCursor = getMessageCursor(); |
| |
| if (messageCursor == null || messageCursor.getCount() == 0) { |
| popOut(); |
| } else { |
| mIsDetached = true; |
| } |
| } |
| |
| /** |
| * Called when the JavaScript reports that it transformed a message. |
| * Sets a flag to true and invalidates the options menu so it will |
| * include the "Revert auto-sizing" menu option. |
| */ |
| public void onConversationTransformed() { |
| mHasConversationBeenTransformed = true; |
| mHandler.post(new FragmentRunnable("invalidateOptionsMenu", this) { |
| @Override |
| public void go() { |
| mActivity.supportInvalidateOptionsMenu(); |
| } |
| }); |
| } |
| |
| /** |
| * Called when the "Revert auto-sizing" option is selected. Default |
| * implementation simply sets a value on whether transforms should be |
| * applied. Derived classes should override this class and force a |
| * re-render so that the conversation renders without |
| */ |
| public void showUntransformedConversation() { |
| // must set the value to true so we don't show the options menu item again |
| mHasConversationTransformBeenReverted = true; |
| } |
| |
| /** |
| * Returns {@code true} if the conversation should be transformed. {@code false}, otherwise. |
| * @return {@code true} if the conversation should be transformed. {@code false}, otherwise. |
| */ |
| public boolean shouldApplyTransforms() { |
| return (mAccount.enableMessageTransforms > 0) && |
| !mHasConversationTransformBeenReverted; |
| } |
| |
| /** |
| * The Print item in the overflow menu of the Conversation view is shown based on the return |
| * from this method. |
| * |
| * @return {@code true} if the conversation can be printed; {@code false} otherwise. |
| */ |
| protected abstract boolean shouldShowPrintInOverflow(); |
| |
| /** |
| * Prints all messages in the conversation. |
| */ |
| protected abstract void printConversation(); |
| |
| // These methods should perform default reply/replyall action on the last message. |
| protected abstract void handleReply(); |
| protected abstract void handleReplyAll(); |
| |
| public boolean shouldAlwaysShowImages() { |
| return (mAccount != null) && (mAccount.settings.showImages == Settings.ShowImages.ALWAYS); |
| } |
| } |