blob: f2122f1c9769898382dd7995a555f29460720492 [file] [log] [blame]
mindypf4fce122012-09-14 15:55:33 -07001/*
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;
mindypf4fce122012-09-14 15:55:33 -070024import android.content.Loader;
25import android.database.Cursor;
mindypf4fce122012-09-14 15:55:33 -070026import android.net.Uri;
27import android.os.Bundle;
mindypff282d02012-09-17 10:33:02 -070028import android.os.Handler;
mindypf4fce122012-09-14 15:55:33 -070029import android.view.Menu;
30import android.view.MenuInflater;
31import android.view.MenuItem;
32
Tony Mantler821e5782014-01-06 15:33:43 -080033import com.android.emailcommon.mail.Address;
mindypf4fce122012-09-14 15:55:33 -070034import com.android.mail.R;
Andy Huang761522c2013-08-08 13:09:11 -070035import com.android.mail.analytics.Analytics;
Andy Huang8f187782012-11-06 17:49:25 -080036import com.android.mail.browse.ConversationAccountController;
Andrew Sapperstein8812d3c2013-06-04 17:06:41 -070037import com.android.mail.browse.ConversationMessage;
mindypf4fce122012-09-14 15:55:33 -070038import com.android.mail.browse.ConversationViewHeader.ConversationViewHeaderCallbacks;
Andy Huang9d3fd922012-09-26 22:23:58 -070039import com.android.mail.browse.MessageCursor;
mindypf4fce122012-09-14 15:55:33 -070040import com.android.mail.browse.MessageCursor.ConversationController;
Paul Westbrookc42ad5e2013-05-09 16:52:15 -070041import com.android.mail.content.ObjectCursor;
42import com.android.mail.content.ObjectCursorLoader;
mindypf4fce122012-09-14 15:55:33 -070043import com.android.mail.providers.Account;
44import com.android.mail.providers.AccountObserver;
mindypf4fce122012-09-14 15:55:33 -070045import com.android.mail.providers.Conversation;
Andy Huange6c9fb62013-11-15 09:56:20 -080046import com.android.mail.providers.Folder;
mindypf4fce122012-09-14 15:55:33 -070047import com.android.mail.providers.ListParams;
Andrew Sapperstein8ec43e82013-12-17 18:27:55 -080048import com.android.mail.providers.Settings;
mindypf4fce122012-09-14 15:55:33 -070049import com.android.mail.providers.UIProvider;
Vikram Aggarwala91d00b2013-01-18 12:00:37 -080050import com.android.mail.providers.UIProvider.CursorStatus;
mindypf4fce122012-09-14 15:55:33 -070051import com.android.mail.utils.LogTag;
52import com.android.mail.utils.LogUtils;
53import com.android.mail.utils.Utils;
mindypf4fce122012-09-14 15:55:33 -070054
Paul Westbrook4d8cad52012-09-21 14:13:49 -070055import java.util.Arrays;
Andy Huang543e7092013-04-22 11:44:56 -070056import java.util.Collections;
57import java.util.HashMap;
mindypf4fce122012-09-14 15:55:33 -070058import java.util.Map;
Andrew Sapperstein376294b2013-06-06 16:04:26 -070059
mindypf4fce122012-09-14 15:55:33 -070060public abstract class AbstractConversationViewFragment extends Fragment implements
Andrew Sapperstein4ddda2f2013-06-10 11:15:38 -070061 ConversationController, ConversationAccountController,
mindypf4fce122012-09-14 15:55:33 -070062 ConversationViewHeaderCallbacks {
63
Andrew Sapperstein606dbd72013-07-30 19:14:23 -070064 protected static final String ARG_ACCOUNT = "account";
mindypf4fce122012-09-14 15:55:33 -070065 public static final String ARG_CONVERSATION = "conversation";
mindypf4fce122012-09-14 15:55:33 -070066 private static final String LOG_TAG = LogTag.getLogTag();
67 protected static final int MESSAGE_LOADER = 0;
68 protected static final int CONTACT_LOADER = 1;
Andy Huang4f347e82014-02-25 17:32:28 -080069 public static final int ATTACHMENT_OPTION1_LOADER = 2;
mindypf4fce122012-09-14 15:55:33 -070070 protected ControllableActivity mActivity;
71 private final MessageLoaderCallbacks mMessageLoaderCallbacks = new MessageLoaderCallbacks();
Andrew Sapperstein376294b2013-06-06 16:04:26 -070072 private ContactLoaderCallbacks mContactLoaderCallbacks;
mindypf4fce122012-09-14 15:55:33 -070073 private MenuItem mChangeFoldersMenuItem;
74 protected Conversation mConversation;
mindypf4fce122012-09-14 15:55:33 -070075 protected String mBaseUri;
76 protected Account mAccount;
Andrew Sapperstein376294b2013-06-06 16:04:26 -070077
78 /**
79 * Must be instantiated in a derived class's onCreate.
80 */
81 protected AbstractConversationWebViewClient mWebViewClient;
Andrew Sapperstein4ddda2f2013-06-10 11:15:38 -070082
Andy Huang543e7092013-04-22 11:44:56 -070083 /**
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>());
mindypf4fce122012-09-14 15:55:33 -070091 private MessageCursor mCursor;
92 private Context mContext;
Andy Huang9d3fd922012-09-26 22:23:58 -070093 /**
94 * A backwards-compatible version of {{@link #getUserVisibleHint()}. Like the framework flag,
95 * this flag is saved and restored.
96 */
97 private boolean mUserVisible;
Andrew Sapperstein376294b2013-06-06 16:04:26 -070098
mindypff282d02012-09-17 10:33:02 -070099 private final Handler mHandler = new Handler();
Vikram Aggarwald82a31f2013-02-05 15:03:00 -0800100 /** True if we want to avoid marking the conversation as viewed and read. */
101 private boolean mSuppressMarkingViewed;
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700102 /**
103 * Parcelable state of the conversation view. Can safely be used without null checking any time
Andy Huang9d3fd922012-09-26 22:23:58 -0700104 * after {@link #onCreate(Bundle)}.
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700105 */
106 protected ConversationViewState mViewState;
107
Scott Kennedy18176782013-02-20 18:30:21 -0800108 private boolean mIsDetached;
109
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700110 private boolean mHasConversationBeenTransformed;
111 private boolean mHasConversationTransformBeenReverted;
112
Andy Huange6c9fb62013-11-15 09:56:20 -0800113 protected boolean mConversationSeen = false;
114
mindypf4fce122012-09-14 15:55:33 -0700115 private final AccountObserver mAccountObserver = new AccountObserver() {
116 @Override
117 public void onChanged(Account newAccount) {
Andy Huangadbf3e82012-10-13 13:30:19 -0700118 final Account oldAccount = mAccount;
mindypf4fce122012-09-14 15:55:33 -0700119 mAccount = newAccount;
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700120 mWebViewClient.setAccount(mAccount);
Andy Huangadbf3e82012-10-13 13:30:19 -0700121 onAccountChanged(newAccount, oldAccount);
mindypf4fce122012-09-14 15:55:33 -0700122 }
123 };
124
Andy Huang9d3fd922012-09-26 22:23:58 -0700125 private static final String BUNDLE_VIEW_STATE =
126 AbstractConversationViewFragment.class.getName() + "viewstate";
Andy Huang9a8bc1e2012-10-23 19:48:25 -0700127 /**
128 * We save the user visible flag so the various transitions that occur during rotation do not
129 * cause unnecessary visibility change.
130 */
Andy Huang9d3fd922012-09-26 22:23:58 -0700131 private static final String BUNDLE_USER_VISIBLE =
132 AbstractConversationViewFragment.class.getName() + "uservisible";
133
Scott Kennedy18176782013-02-20 18:30:21 -0800134 private static final String BUNDLE_DETACHED =
135 AbstractConversationViewFragment.class.getName() + "detached";
136
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700137 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
Andrew Sapperstein606dbd72013-07-30 19:14:23 -0700142 public static Bundle makeBasicArgs(Account account) {
mindypf4fce122012-09-14 15:55:33 -0700143 Bundle args = new Bundle();
144 args.putParcelable(ARG_ACCOUNT, account);
mindypf4fce122012-09-14 15:55:33 -0700145 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 */
Vikram Aggarwald82a31f2013-02-05 15:03:00 -0800160 protected void markUnread() {
161 // Do not automatically mark this conversation viewed and read.
162 mSuppressMarkingViewed = true;
163 }
mindypf4fce122012-09-14 15:55:33 -0700164
165 /**
166 * Subclasses must override this, since they may want to display a single or
167 * many messages related to this conversation.
168 */
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700169 protected abstract void onMessageCursorLoadFinished(
170 Loader<ObjectCursor<ConversationMessage>> loader,
Andy Huang014ea4c2012-09-25 14:50:54 -0700171 MessageCursor newCursor, MessageCursor oldCursor);
mindypf4fce122012-09-14 15:55:33 -0700172
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 */
Andy Huangadbf3e82012-10-13 13:30:19 -0700185 protected abstract void onAccountChanged(Account newAccount, Account oldAccount);
mindypf4fce122012-09-14 15:55:33 -0700186
187 @Override
188 public void onCreate(Bundle savedState) {
189 super.onCreate(savedState);
190
Andrew Sapperstein606dbd72013-07-30 19:14:23 -0700191 parseArguments();
192 setBaseUri();
Paul Westbrookba4cce62012-09-28 10:24:20 -0700193
mindypf4fce122012-09-14 15:55:33 -0700194 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);
Andy Huang9d3fd922012-09-26 22:23:58 -0700197
198 if (savedState != null) {
199 mViewState = savedState.getParcelable(BUNDLE_VIEW_STATE);
200 mUserVisible = savedState.getBoolean(BUNDLE_USER_VISIBLE);
Scott Kennedy18176782013-02-20 18:30:21 -0800201 mIsDetached = savedState.getBoolean(BUNDLE_DETACHED, false);
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700202 mHasConversationBeenTransformed =
203 savedState.getBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_TRANSFORMED, false);
204 mHasConversationTransformBeenReverted =
205 savedState.getBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_REVERTED, false);
Andy Huang9d3fd922012-09-26 22:23:58 -0700206 } else {
207 mViewState = getNewViewState();
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700208 mHasConversationBeenTransformed = false;
209 mHasConversationTransformBeenReverted = false;
Andy Huang9d3fd922012-09-26 22:23:58 -0700210 }
mindypf4fce122012-09-14 15:55:33 -0700211 }
212
Andrew Sapperstein606dbd72013-07-30 19:14:23 -0700213 /**
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() {
Ray Chenee04e662014-07-23 10:13:27 +0200227 mBaseUri = buildBaseUri(getContext(), mAccount, mConversation);
Andrew Sapperstein562c5ba2013-10-09 18:31:50 -0700228 }
229
Ray Chen4b0c0122014-07-11 15:24:54 +0200230 public static String buildBaseUri(Context context, Account account, Conversation conversation) {
Andrew Sapperstein606dbd72013-07-30 19:14:23 -0700231 // 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.
Paul Westbrookfd792372014-07-30 02:28:16 +0000233 return "x-thread://" + account.getAccountId().hashCode() + "/" + conversation.id;
Andrew Sapperstein606dbd72013-07-30 19:14:23 -0700234 }
235
Andy Huang9e4ca792013-02-28 14:33:43 -0800236 @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
mindypf4fce122012-09-14 15:55:33 -0700246 @Override
247 public void onActivityCreated(Bundle savedInstanceState) {
248 super.onActivityCreated(savedInstanceState);
Vikram Aggarwal8fe8ed42012-09-18 11:40:08 -0700249 final Activity activity = getActivity();
mindypf4fce122012-09-14 15:55:33 -0700250 if (!(activity instanceof ControllableActivity)) {
251 LogUtils.wtf(LOG_TAG, "ConversationViewFragment expects only a ControllableActivity to"
252 + "create it. Cannot proceed.");
253 }
Vikram Aggarwal8fe8ed42012-09-18 11:40:08 -0700254 if (activity == null || activity.isFinishing()) {
mindypf4fce122012-09-14 15:55:33 -0700255 // Activity is finishing, just bail.
256 return;
257 }
Vikram Aggarwal8fe8ed42012-09-18 11:40:08 -0700258 mActivity = (ControllableActivity) activity;
mindypf4fce122012-09-14 15:55:33 -0700259 mContext = activity.getApplicationContext();
Andy Huangb622d2b2013-06-12 13:47:17 -0700260 mWebViewClient.setActivity(activity);
mindypf4fce122012-09-14 15:55:33 -0700261 mAccount = mAccountObserver.initialize(mActivity.getAccountController());
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700262 mWebViewClient.setAccount(mAccount);
mindypf4fce122012-09-14 15:55:33 -0700263 }
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
Andy Huang02133aa2012-11-08 19:50:57 -0800275 @Override
mindypf4fce122012-09-14 15:55:33 -0700276 public Conversation getConversation() {
277 return mConversation;
278 }
279
280 @Override
281 public MessageCursor getMessageCursor() {
282 return mCursor;
283 }
284
mindypff282d02012-09-17 10:33:02 -0700285 public Handler getHandler() {
286 return mHandler;
287 }
288
mindypf4fce122012-09-14 15:55:33 -0700289 public MessageLoaderCallbacks getMessageLoaderCallbacks() {
290 return mMessageLoaderCallbacks;
291 }
292
293 public ContactLoaderCallbacks getContactInfoSource() {
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700294 if (mContactLoaderCallbacks == null) {
Andrew Sapperstein8913ca62014-05-14 15:03:40 -0700295 mContactLoaderCallbacks = mActivity.getContactLoaderCallbacks();
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700296 }
mindypf4fce122012-09-14 15:55:33 -0700297 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);
Andrew Sapperstein6c570db2013-08-06 17:21:36 -0700308 mChangeFoldersMenuItem = menu.findItem(R.id.change_folders);
mindypf4fce122012-09-14 15:55:33 -0700309 }
310
311 @Override
mindypf4fce122012-09-14 15:55:33 -0700312 public boolean onOptionsItemSelected(MenuItem item) {
Andy Huangbb9dd6b2013-02-28 17:13:54 -0800313 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)) {
Tony Mantler54a90572014-03-03 16:23:08 -0800324 LogUtils.e(LOG_TAG, "%s", Utils.dumpFragment(this));
Andy Huangbb9dd6b2013-02-28 17:13:54 -0800325 }
326 return false;
327 }
328
mindypf4fce122012-09-14 15:55:33 -0700329 boolean handled = false;
Scott Kennedy2b9d80e2013-07-30 23:03:45 -0700330 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;
Andrew Sapperstein6293ef02013-10-07 18:22:10 -0700337 } else if (itemId == R.id.print_all) {
Andrew Sapperstein5c1692a2013-09-16 11:56:13 -0700338 printConversation();
339 handled = true;
mindypf4fce122012-09-14 15:55:33 -0700340 }
341 return handled;
342 }
343
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700344 @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);
James Lemieux7cad2802014-01-09 15:00:53 -0800349
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 }
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700362 }
363
Andrew Sapperstein4ddda2f2013-06-10 11:15:38 -0700364 abstract boolean supportsMessageTransforms();
365
mindypf4fce122012-09-14 15:55:33 -0700366 // 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 }
mindypf4fce122012-09-14 15:55:33 -0700375 // END conversation header callbacks
376
377 @Override
Andy Huang761522c2013-08-08 13:09:11 -0700378 public void onStart() {
379 super.onStart();
380
381 Analytics.getInstance().sendView(getClass().getName());
382 }
383
384 @Override
Andy Huang9d3fd922012-09-26 22:23:58 -0700385 public void onSaveInstanceState(Bundle outState) {
386 if (mViewState != null) {
387 outState.putParcelable(BUNDLE_VIEW_STATE, mViewState);
388 }
389 outState.putBoolean(BUNDLE_USER_VISIBLE, mUserVisible);
Scott Kennedy18176782013-02-20 18:30:21 -0800390 outState.putBoolean(BUNDLE_DETACHED, mIsDetached);
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700391 outState.putBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_TRANSFORMED,
392 mHasConversationBeenTransformed);
393 outState.putBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_REVERTED,
394 mHasConversationTransformBeenReverted);
Andy Huang9d3fd922012-09-26 22:23:58 -0700395 }
396
397 @Override
mindypf4fce122012-09-14 15:55:33 -0700398 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;
mindypbc4142f2012-09-19 09:29:49 -0700411 MessageCursor cursor = getMessageCursor();
mindyp0b9b48c2012-09-19 10:00:51 -0700412 if (mUserVisible && (cursor != null && cursor.isLoaded() && cursor.getCount() == 0)) {
mindypbc4142f2012-09-19 09:29:49 -0700413 // Pop back to conversation list and show error.
414 onError();
415 return;
416 }
mindypf4fce122012-09-14 15:55:33 -0700417 onUserVisibleHintChanged();
418 }
419 }
420
Andy Huang9d3fd922012-09-26 22:23:58 -0700421 public boolean isUserVisible() {
422 return mUserVisible;
423 }
424
Andy Huang243c2362013-03-01 17:50:35 -0800425 protected void timerMark(String msg) {
426 if (isUserVisible()) {
427 Utils.sConvLoadTimer.mark(msg);
428 }
429 }
430
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700431 private class MessageLoaderCallbacks
432 implements LoaderManager.LoaderCallbacks<ObjectCursor<ConversationMessage>> {
mindypf4fce122012-09-14 15:55:33 -0700433
434 @Override
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700435 public Loader<ObjectCursor<ConversationMessage>> onCreateLoader(int id, Bundle args) {
Andy Huang02133aa2012-11-08 19:50:57 -0800436 return new MessageLoader(mActivity.getActivityContext(), mConversation.messageListUri);
mindypf4fce122012-09-14 15:55:33 -0700437 }
438
439 @Override
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700440 public void onLoadFinished(Loader<ObjectCursor<ConversationMessage>> loader,
441 ObjectCursor<ConversationMessage> data) {
mindypf4fce122012-09-14 15:55:33 -0700442 // ignore truly duplicate results
443 // this can happen when restoring after rotation
444 if (mCursor == data) {
445 return;
446 } else {
Andy Huang6766b6e2012-09-28 12:43:52 -0700447 final MessageCursor messageCursor = (MessageCursor) data;
mindypf4fce122012-09-14 15:55:33 -0700448
Andy Huang02133aa2012-11-08 19:50:57 -0800449 // bind the cursor to this fragment so it can access to the current list controller
450 messageCursor.setController(AbstractConversationViewFragment.this);
451
mindypf4fce122012-09-14 15:55:33 -0700452 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
453 LogUtils.d(LOG_TAG, "LOADED CONVERSATION= %s", messageCursor.getDebugDump());
454 }
455
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800456 // We have no messages: exit conversation view.
457 if (messageCursor.getCount() == 0
Scott Kennedy18176782013-02-20 18:30:21 -0800458 && (!CursorStatus.isWaitingForResults(messageCursor.getStatus())
459 || mIsDetached)) {
mindypf4fce122012-09-14 15:55:33 -0700460 if (mUserVisible) {
mindypbc4142f2012-09-19 09:29:49 -0700461 onError();
mindypf4fce122012-09-14 15:55:33 -0700462 } 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"
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700469 + " in anticipation of conv cursor update. c=%s",
470 mConversation.uri);
mindypf4fce122012-09-14 15:55:33 -0700471 }
Andy Huang233d4352012-10-18 14:00:24 -0700472 // 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;
mindypf4fce122012-09-14 15:55:33 -0700476 return;
477 }
478
479 // ignore cursors that are still loading results
480 if (!messageCursor.isLoaded()) {
Andy Huang233d4352012-10-18 14:00:24 -0700481 // 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;
mindypf4fce122012-09-14 15:55:33 -0700486 return;
487 }
Andy Huang014ea4c2012-09-25 14:50:54 -0700488 final MessageCursor oldCursor = mCursor;
Andy Huang6766b6e2012-09-28 12:43:52 -0700489 mCursor = messageCursor;
Andy Huang014ea4c2012-09-25 14:50:54 -0700490 onMessageCursorLoadFinished(loader, mCursor, oldCursor);
mindypf4fce122012-09-14 15:55:33 -0700491 }
492 }
493
494 @Override
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700495 public void onLoaderReset(Loader<ObjectCursor<ConversationMessage>> loader) {
mindypf4fce122012-09-14 15:55:33 -0700496 mCursor = null;
497 }
498
499 }
500
mindypbc4142f2012-09-19 09:29:49 -0700501 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
Scott Kennedy18176782013-02-20 18:30:21 -0800513 popOut();
514 }
mindypbc4142f2012-09-19 09:29:49 -0700515
Scott Kennedy18176782013-02-20 18:30:21 -0800516 private void popOut() {
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700517 mHandler.post(new FragmentRunnable("popOut", this) {
mindypbc4142f2012-09-19 09:29:49 -0700518 @Override
Andy Huang9a8bc1e2012-10-23 19:48:25 -0700519 public void go() {
Alice Yang7729a9a2013-05-23 18:09:09 -0700520 if (mActivity != null) {
521 mActivity.getListHandler()
522 .onConversationSelected(null, true /* inLoaderCallbacks */);
523 }
mindypbc4142f2012-09-19 09:29:49 -0700524 }
mindypbc4142f2012-09-19 09:29:49 -0700525 });
526 }
527
Andy Huange6c9fb62013-11-15 09:56:20 -0800528 /**
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
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700553 protected void onConversationSeen() {
Scott Kennedy919d01a2013-05-07 16:13:29 -0700554 LogUtils.d(LOG_TAG, "AbstractConversationViewFragment#onConversationSeen()");
555
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700556 // 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
Andy Huange6c9fb62013-11-15 09:56:20 -0800563 // 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
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700570 mViewState.setInfoForConversation(mConversation);
571
Scott Kennedy919d01a2013-05-07 16:13:29 -0700572 LogUtils.d(LOG_TAG, "onConversationSeen() - mSuppressMarkingViewed = %b",
573 mSuppressMarkingViewed);
Vikram Aggarwald82a31f2013-02-05 15:03:00 -0800574 // 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)
Paul Westbrook21452292013-04-15 18:51:07 -0700583 final MessageCursor cursor = getMessageCursor();
Scott Kennedy919d01a2013-05-07 16:13:29 -0700584 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());
Vikram Aggarwald82a31f2013-02-05 15:03:00 -0800588 if (!mConversation.isViewed() || (cursor != null && !cursor.isConversationRead())) {
589 // Mark the conversation viewed and read.
590 activity.getConversationUpdater()
591 .markConversationsRead(Arrays.asList(mConversation), true, true);
Yu Ping Hu7c909c72013-01-18 11:58:01 -0800592
Vikram Aggarwald82a31f2013-02-05 15:03:00 -0800593 // 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
Paul Westbrook21452292013-04-15 18:51:07 -0700595 if (cursor != null && !cursor.isClosed()) {
Vikram Aggarwald82a31f2013-02-05 15:03:00 -0800596 cursor.markMessagesRead();
597 }
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700598 }
599 }
Scott Kennedy3b965d72013-06-25 14:36:55 -0700600 activity.getListHandler().onConversationSeen();
Andy Huange6c9fb62013-11-15 09:56:20 -0800601
602 mConversationSeen = true;
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700603 }
604
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700605 protected ConversationViewState getNewViewState() {
606 return new ConversationViewState();
607 }
608
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700609 private static class MessageLoader extends ObjectCursorLoader<ConversationMessage> {
mindypf4fce122012-09-14 15:55:33 -0700610 private boolean mDeliveredFirstResults = false;
mindypf4fce122012-09-14 15:55:33 -0700611
Andy Huang02133aa2012-11-08 19:50:57 -0800612 public MessageLoader(Context c, Uri messageListUri) {
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700613 super(c, messageListUri, UIProvider.MESSAGE_PROJECTION, ConversationMessage.FACTORY);
mindypf4fce122012-09-14 15:55:33 -0700614 }
615
616 @Override
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700617 public void deliverResult(ObjectCursor<ConversationMessage> result) {
mindypf4fce122012-09-14 15:55:33 -0700618 // 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 }
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700640
641 @Override
642 protected ObjectCursor<ConversationMessage> getObjectCursor(Cursor inner) {
643 return new MessageCursor(inner);
644 }
mindypf4fce122012-09-14 15:55:33 -0700645 }
646
mindyp26d4d2d2012-09-18 17:30:32 -0700647 public abstract void onConversationUpdated(Conversation conversation);
648
Scott Kennedy18176782013-02-20 18:30:21 -0800649 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 }
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700660
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;
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700668 mHandler.post(new FragmentRunnable("invalidateOptionsMenu", this) {
Andrew Sappersteinae92e152013-05-03 13:55:18 -0700669 @Override
670 public void go() {
Andrew Sapperstein52882ff2014-07-27 12:30:18 -0700671 mActivity.supportInvalidateOptionsMenu();
Andrew Sappersteinae92e152013-05-03 13:55:18 -0700672 }
673 });
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700674 }
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() {
Alice Yang3617b412013-05-10 00:30:07 -0700692 return (mAccount.enableMessageTransforms > 0) &&
693 !mHasConversationTransformBeenReverted;
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700694 }
Andrew Sapperstein5c1692a2013-09-16 11:56:13 -0700695
James Lemieux7cad2802014-01-09 15:00:53 -0800696 /**
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 */
Andrew Sapperstein5c1692a2013-09-16 11:56:13 -0700707 protected abstract void printConversation();
Andrew Sapperstein8ec43e82013-12-17 18:27:55 -0800708
709 public boolean shouldAlwaysShowImages() {
710 return (mAccount != null) && (mAccount.settings.showImages == Settings.ShowImages.ALWAYS);
711 }
mindypf4fce122012-09-14 15:55:33 -0700712}