blob: 674d7d938fba0df468a99cdb44219dbb4c6141dd [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;
Andrew Sappersteine8221482013-10-02 18:14:58 -070043import com.android.mail.preferences.AccountPreferences;
mindypf4fce122012-09-14 15:55:33 -070044import com.android.mail.providers.Account;
45import com.android.mail.providers.AccountObserver;
mindypf4fce122012-09-14 15:55:33 -070046import com.android.mail.providers.Conversation;
Andy Huange6c9fb62013-11-15 09:56:20 -080047import com.android.mail.providers.Folder;
mindypf4fce122012-09-14 15:55:33 -070048import com.android.mail.providers.ListParams;
Andrew Sapperstein8ec43e82013-12-17 18:27:55 -080049import com.android.mail.providers.Settings;
mindypf4fce122012-09-14 15:55:33 -070050import com.android.mail.providers.UIProvider;
Vikram Aggarwala91d00b2013-01-18 12:00:37 -080051import com.android.mail.providers.UIProvider.CursorStatus;
mindypf4fce122012-09-14 15:55:33 -070052import com.android.mail.utils.LogTag;
53import com.android.mail.utils.LogUtils;
54import com.android.mail.utils.Utils;
mindypf4fce122012-09-14 15:55:33 -070055
Paul Westbrook4d8cad52012-09-21 14:13:49 -070056import java.util.Arrays;
Andy Huang543e7092013-04-22 11:44:56 -070057import java.util.Collections;
58import java.util.HashMap;
mindypf4fce122012-09-14 15:55:33 -070059import java.util.Map;
Andrew Sapperstein376294b2013-06-06 16:04:26 -070060
mindypf4fce122012-09-14 15:55:33 -070061public abstract class AbstractConversationViewFragment extends Fragment implements
Andrew Sapperstein4ddda2f2013-06-10 11:15:38 -070062 ConversationController, ConversationAccountController,
mindypf4fce122012-09-14 15:55:33 -070063 ConversationViewHeaderCallbacks {
64
Andrew Sapperstein606dbd72013-07-30 19:14:23 -070065 protected static final String ARG_ACCOUNT = "account";
mindypf4fce122012-09-14 15:55:33 -070066 public static final String ARG_CONVERSATION = "conversation";
mindypf4fce122012-09-14 15:55:33 -070067 private static final String LOG_TAG = LogTag.getLogTag();
68 protected static final int MESSAGE_LOADER = 0;
69 protected static final int CONTACT_LOADER = 1;
70 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() {
Andrew Sapperstein562c5ba2013-10-09 18:31:50 -0700227 mBaseUri = buildBaseUri(mAccount, mConversation);
228 }
229
230 public static String buildBaseUri(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.
Andrew Sapperstein562c5ba2013-10-09 18:31:50 -0700233 return "x-thread://" + account.getEmailAddress().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) {
295 mContactLoaderCallbacks = new ContactLoaderCallbacks(mActivity.getActivityContext());
296 }
mindypf4fce122012-09-14 15:55:33 -0700297 return mContactLoaderCallbacks;
298 }
299
300 @Override
301 public Account getAccount() {
302 return mAccount;
303 }
304
305 @Override
Andrew Sappersteine8221482013-10-02 18:14:58 -0700306 public AccountPreferences getAccountPreferences() {
Tony Mantlercfb969b2013-11-25 09:45:12 -0800307 return AccountPreferences.get(getContext(), mAccount.getEmailAddress());
Andrew Sappersteine8221482013-10-02 18:14:58 -0700308 }
309
310 @Override
mindypf4fce122012-09-14 15:55:33 -0700311 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
312 super.onCreateOptionsMenu(menu, inflater);
Andrew Sapperstein6c570db2013-08-06 17:21:36 -0700313 mChangeFoldersMenuItem = menu.findItem(R.id.change_folders);
mindypf4fce122012-09-14 15:55:33 -0700314 }
315
316 @Override
mindypf4fce122012-09-14 15:55:33 -0700317 public boolean onOptionsItemSelected(MenuItem item) {
Andy Huangbb9dd6b2013-02-28 17:13:54 -0800318 if (!isUserVisible()) {
319 // Unclear how this is happening. Current theory is that this fragment was scheduled
320 // to be removed, but the remove transaction failed. When the Activity is later
321 // restored, the FragmentManager restores this fragment, but Fragment.mMenuVisible is
322 // stuck at its initial value (true), which makes this zombie fragment eligible for
323 // menu item clicks.
324 //
325 // Work around this by relying on the (properly restored) extra user visible hint.
326 LogUtils.e(LOG_TAG,
327 "ACVF ignoring onOptionsItemSelected b/c userVisibleHint is false. f=%s", this);
328 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
Tony Mantler54a90572014-03-03 16:23:08 -0800329 LogUtils.e(LOG_TAG, "%s", Utils.dumpFragment(this));
Andy Huangbb9dd6b2013-02-28 17:13:54 -0800330 }
331 return false;
332 }
333
mindypf4fce122012-09-14 15:55:33 -0700334 boolean handled = false;
Scott Kennedy2b9d80e2013-07-30 23:03:45 -0700335 final int itemId = item.getItemId();
336 if (itemId == R.id.inside_conversation_unread) {
337 markUnread();
338 handled = true;
339 } else if (itemId == R.id.show_original) {
340 showUntransformedConversation();
341 handled = true;
Andrew Sapperstein6293ef02013-10-07 18:22:10 -0700342 } else if (itemId == R.id.print_all) {
Andrew Sapperstein5c1692a2013-09-16 11:56:13 -0700343 printConversation();
344 handled = true;
mindypf4fce122012-09-14 15:55:33 -0700345 }
346 return handled;
347 }
348
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700349 @Override
350 public void onPrepareOptionsMenu(Menu menu) {
351 // Only show option if we support message transforms and message has been transformed.
352 Utils.setMenuItemVisibility(menu, R.id.show_original, supportsMessageTransforms() &&
353 mHasConversationBeenTransformed && !mHasConversationTransformBeenReverted);
James Lemieux7cad2802014-01-09 15:00:53 -0800354
355 final MenuItem printMenuItem = menu.findItem(R.id.print_all);
356 if (printMenuItem != null) {
357 // compute the visibility of the print menu item
358 printMenuItem.setVisible(Utils.isRunningKitkatOrLater() && shouldShowPrintInOverflow());
359
360 // compute the text displayed on the print menu item
361 if (mConversation.getNumMessages() == 1) {
362 printMenuItem.setTitle(R.string.print);
363 } else {
364 printMenuItem.setTitle(R.string.print_all);
365 }
366 }
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700367 }
368
Andrew Sapperstein4ddda2f2013-06-10 11:15:38 -0700369 abstract boolean supportsMessageTransforms();
370
mindypf4fce122012-09-14 15:55:33 -0700371 // BEGIN conversation header callbacks
372 @Override
373 public void onFoldersClicked() {
374 if (mChangeFoldersMenuItem == null) {
375 LogUtils.e(LOG_TAG, "unable to open 'change folders' dialog for a conversation");
376 return;
377 }
378 mActivity.onOptionsItemSelected(mChangeFoldersMenuItem);
379 }
mindypf4fce122012-09-14 15:55:33 -0700380 // END conversation header callbacks
381
382 @Override
Andy Huang761522c2013-08-08 13:09:11 -0700383 public void onStart() {
384 super.onStart();
385
386 Analytics.getInstance().sendView(getClass().getName());
387 }
388
389 @Override
Andy Huang9d3fd922012-09-26 22:23:58 -0700390 public void onSaveInstanceState(Bundle outState) {
391 if (mViewState != null) {
392 outState.putParcelable(BUNDLE_VIEW_STATE, mViewState);
393 }
394 outState.putBoolean(BUNDLE_USER_VISIBLE, mUserVisible);
Scott Kennedy18176782013-02-20 18:30:21 -0800395 outState.putBoolean(BUNDLE_DETACHED, mIsDetached);
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700396 outState.putBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_TRANSFORMED,
397 mHasConversationBeenTransformed);
398 outState.putBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_REVERTED,
399 mHasConversationTransformBeenReverted);
Andy Huang9d3fd922012-09-26 22:23:58 -0700400 }
401
402 @Override
mindypf4fce122012-09-14 15:55:33 -0700403 public void onDestroyView() {
404 super.onDestroyView();
405 mAccountObserver.unregisterAndDestroy();
406 }
407
408 /**
409 * {@link #setUserVisibleHint(boolean)} only works on API >= 15, so implement our own for
410 * reliability on older platforms.
411 */
412 public void setExtraUserVisibleHint(boolean isVisibleToUser) {
413 LogUtils.v(LOG_TAG, "in CVF.setHint, val=%s (%s)", isVisibleToUser, this);
414 if (mUserVisible != isVisibleToUser) {
415 mUserVisible = isVisibleToUser;
mindypbc4142f2012-09-19 09:29:49 -0700416 MessageCursor cursor = getMessageCursor();
mindyp0b9b48c2012-09-19 10:00:51 -0700417 if (mUserVisible && (cursor != null && cursor.isLoaded() && cursor.getCount() == 0)) {
mindypbc4142f2012-09-19 09:29:49 -0700418 // Pop back to conversation list and show error.
419 onError();
420 return;
421 }
mindypf4fce122012-09-14 15:55:33 -0700422 onUserVisibleHintChanged();
423 }
424 }
425
Andy Huang9d3fd922012-09-26 22:23:58 -0700426 public boolean isUserVisible() {
427 return mUserVisible;
428 }
429
Andy Huang243c2362013-03-01 17:50:35 -0800430 protected void timerMark(String msg) {
431 if (isUserVisible()) {
432 Utils.sConvLoadTimer.mark(msg);
433 }
434 }
435
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700436 private class MessageLoaderCallbacks
437 implements LoaderManager.LoaderCallbacks<ObjectCursor<ConversationMessage>> {
mindypf4fce122012-09-14 15:55:33 -0700438
439 @Override
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700440 public Loader<ObjectCursor<ConversationMessage>> onCreateLoader(int id, Bundle args) {
Andy Huang02133aa2012-11-08 19:50:57 -0800441 return new MessageLoader(mActivity.getActivityContext(), mConversation.messageListUri);
mindypf4fce122012-09-14 15:55:33 -0700442 }
443
444 @Override
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700445 public void onLoadFinished(Loader<ObjectCursor<ConversationMessage>> loader,
446 ObjectCursor<ConversationMessage> data) {
mindypf4fce122012-09-14 15:55:33 -0700447 // ignore truly duplicate results
448 // this can happen when restoring after rotation
449 if (mCursor == data) {
450 return;
451 } else {
Andy Huang6766b6e2012-09-28 12:43:52 -0700452 final MessageCursor messageCursor = (MessageCursor) data;
mindypf4fce122012-09-14 15:55:33 -0700453
Andy Huang02133aa2012-11-08 19:50:57 -0800454 // bind the cursor to this fragment so it can access to the current list controller
455 messageCursor.setController(AbstractConversationViewFragment.this);
456
mindypf4fce122012-09-14 15:55:33 -0700457 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
458 LogUtils.d(LOG_TAG, "LOADED CONVERSATION= %s", messageCursor.getDebugDump());
459 }
460
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800461 // We have no messages: exit conversation view.
462 if (messageCursor.getCount() == 0
Scott Kennedy18176782013-02-20 18:30:21 -0800463 && (!CursorStatus.isWaitingForResults(messageCursor.getStatus())
464 || mIsDetached)) {
mindypf4fce122012-09-14 15:55:33 -0700465 if (mUserVisible) {
mindypbc4142f2012-09-19 09:29:49 -0700466 onError();
mindypf4fce122012-09-14 15:55:33 -0700467 } else {
468 // we expect that the pager adapter will remove this
469 // conversation fragment on its own due to a separate
470 // conversation cursor update (we might get here if the
471 // message list update fires first. nothing to do
472 // because we expect to be torn down soon.)
473 LogUtils.i(LOG_TAG, "CVF: offscreen conv has no messages, ignoring update"
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700474 + " in anticipation of conv cursor update. c=%s",
475 mConversation.uri);
mindypf4fce122012-09-14 15:55:33 -0700476 }
Andy Huang233d4352012-10-18 14:00:24 -0700477 // existing mCursor will imminently be closed, must stop referencing it
478 // since we expect to be kicked out soon, it doesn't matter what mCursor
479 // becomes
480 mCursor = null;
mindypf4fce122012-09-14 15:55:33 -0700481 return;
482 }
483
484 // ignore cursors that are still loading results
485 if (!messageCursor.isLoaded()) {
Andy Huang233d4352012-10-18 14:00:24 -0700486 // existing mCursor will imminently be closed, must stop referencing it
487 // in this case, the new cursor is also no good, and since don't expect to get
488 // here except in initial load situations, it's safest to just ensure the
489 // reference is null
490 mCursor = null;
mindypf4fce122012-09-14 15:55:33 -0700491 return;
492 }
Andy Huang014ea4c2012-09-25 14:50:54 -0700493 final MessageCursor oldCursor = mCursor;
Andy Huang6766b6e2012-09-28 12:43:52 -0700494 mCursor = messageCursor;
Andy Huang014ea4c2012-09-25 14:50:54 -0700495 onMessageCursorLoadFinished(loader, mCursor, oldCursor);
mindypf4fce122012-09-14 15:55:33 -0700496 }
497 }
498
499 @Override
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700500 public void onLoaderReset(Loader<ObjectCursor<ConversationMessage>> loader) {
mindypf4fce122012-09-14 15:55:33 -0700501 mCursor = null;
502 }
503
504 }
505
mindypbc4142f2012-09-19 09:29:49 -0700506 private void onError() {
507 // need to exit this view- conversation may have been
508 // deleted, or for whatever reason is now invalid (e.g.
509 // discard single draft)
510 //
511 // N.B. this may involve a fragment transaction, which
512 // FragmentManager will refuse to execute directly
513 // within onLoadFinished. Make sure the controller knows.
514 LogUtils.i(LOG_TAG, "CVF: visible conv has no messages, exiting conv mode");
515 // TODO(mindyp): handle ERROR status by showing an error
516 // message to the user that there are no messages in
517 // this conversation
Scott Kennedy18176782013-02-20 18:30:21 -0800518 popOut();
519 }
mindypbc4142f2012-09-19 09:29:49 -0700520
Scott Kennedy18176782013-02-20 18:30:21 -0800521 private void popOut() {
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700522 mHandler.post(new FragmentRunnable("popOut", this) {
mindypbc4142f2012-09-19 09:29:49 -0700523 @Override
Andy Huang9a8bc1e2012-10-23 19:48:25 -0700524 public void go() {
Alice Yang7729a9a2013-05-23 18:09:09 -0700525 if (mActivity != null) {
526 mActivity.getListHandler()
527 .onConversationSelected(null, true /* inLoaderCallbacks */);
528 }
mindypbc4142f2012-09-19 09:29:49 -0700529 }
mindypbc4142f2012-09-19 09:29:49 -0700530 });
531 }
532
Andy Huange6c9fb62013-11-15 09:56:20 -0800533 /**
534 * @see Folder#getTypeDescription()
535 */
536 protected String getCurrentFolderTypeDesc() {
537 final Folder currFolder;
538 if (mActivity != null) {
539 currFolder = mActivity.getFolderController().getFolder();
540 } else {
541 currFolder = null;
542 }
543 final String folderStr;
544 if (currFolder != null) {
545 folderStr = currFolder.getTypeDescription();
546 } else {
547 folderStr = "unknown_folder";
548 }
549 return folderStr;
550 }
551
552 private void logConversationView() {
553 final String folderStr = getCurrentFolderTypeDesc();
554 Analytics.getInstance().sendEvent("view_conversation", folderStr,
555 mConversation.isRemote ? "unsynced" : "synced", mConversation.getNumMessages());
556 }
557
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700558 protected void onConversationSeen() {
Scott Kennedy919d01a2013-05-07 16:13:29 -0700559 LogUtils.d(LOG_TAG, "AbstractConversationViewFragment#onConversationSeen()");
560
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700561 // Ignore unsafe calls made after a fragment is detached from an activity
562 final ControllableActivity activity = (ControllableActivity) getActivity();
563 if (activity == null) {
564 LogUtils.w(LOG_TAG, "ignoring onConversationSeen for conv=%s", mConversation.id);
565 return;
566 }
567
Andy Huange6c9fb62013-11-15 09:56:20 -0800568 // this method is called 2x on rotation; debounce this a bit so as not to
569 // dramatically skew analytics data too much. Ideally, it should be called zero times
570 // on rotation...
571 if (!mConversationSeen) {
572 logConversationView();
573 }
574
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700575 mViewState.setInfoForConversation(mConversation);
576
Scott Kennedy919d01a2013-05-07 16:13:29 -0700577 LogUtils.d(LOG_TAG, "onConversationSeen() - mSuppressMarkingViewed = %b",
578 mSuppressMarkingViewed);
Vikram Aggarwald82a31f2013-02-05 15:03:00 -0800579 // In most circumstances we want to mark the conversation as viewed and read, since the
580 // user has read it. However, if the user has already marked the conversation unread, we
581 // do not want a later mark-read operation to undo this. So we check this variable which
582 // is set in #markUnread() which suppresses automatic mark-read.
583 if (!mSuppressMarkingViewed) {
584 // mark viewed/read if not previously marked viewed by this conversation view,
585 // or if unread messages still exist in the message list cursor
586 // we don't want to keep marking viewed on rotation or restore
587 // but we do want future re-renders to mark read (e.g. "New message from X" case)
Paul Westbrook21452292013-04-15 18:51:07 -0700588 final MessageCursor cursor = getMessageCursor();
Scott Kennedy919d01a2013-05-07 16:13:29 -0700589 LogUtils.d(LOG_TAG, "onConversationSeen() - mConversation.isViewed() = %b, "
590 + "cursor null = %b, cursor.isConversationRead() = %b",
591 mConversation.isViewed(), cursor == null,
592 cursor != null && cursor.isConversationRead());
Vikram Aggarwald82a31f2013-02-05 15:03:00 -0800593 if (!mConversation.isViewed() || (cursor != null && !cursor.isConversationRead())) {
594 // Mark the conversation viewed and read.
595 activity.getConversationUpdater()
596 .markConversationsRead(Arrays.asList(mConversation), true, true);
Yu Ping Hu7c909c72013-01-18 11:58:01 -0800597
Vikram Aggarwald82a31f2013-02-05 15:03:00 -0800598 // and update the Message objects in the cursor so the next time a cursor update
599 // happens with these messages marked read, we know to ignore it
Paul Westbrook21452292013-04-15 18:51:07 -0700600 if (cursor != null && !cursor.isClosed()) {
Vikram Aggarwald82a31f2013-02-05 15:03:00 -0800601 cursor.markMessagesRead();
602 }
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700603 }
604 }
Scott Kennedy3b965d72013-06-25 14:36:55 -0700605 activity.getListHandler().onConversationSeen();
Andy Huange6c9fb62013-11-15 09:56:20 -0800606
607 mConversationSeen = true;
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700608 }
609
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700610 protected ConversationViewState getNewViewState() {
611 return new ConversationViewState();
612 }
613
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700614 private static class MessageLoader extends ObjectCursorLoader<ConversationMessage> {
mindypf4fce122012-09-14 15:55:33 -0700615 private boolean mDeliveredFirstResults = false;
mindypf4fce122012-09-14 15:55:33 -0700616
Andy Huang02133aa2012-11-08 19:50:57 -0800617 public MessageLoader(Context c, Uri messageListUri) {
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700618 super(c, messageListUri, UIProvider.MESSAGE_PROJECTION, ConversationMessage.FACTORY);
mindypf4fce122012-09-14 15:55:33 -0700619 }
620
621 @Override
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700622 public void deliverResult(ObjectCursor<ConversationMessage> result) {
mindypf4fce122012-09-14 15:55:33 -0700623 // We want to deliver these results, and then we want to make sure
624 // that any subsequent
625 // queries do not hit the network
626 super.deliverResult(result);
627
628 if (!mDeliveredFirstResults) {
629 mDeliveredFirstResults = true;
630 Uri uri = getUri();
631
632 // Create a ListParams that tells the provider to not hit the
633 // network
634 final ListParams listParams = new ListParams(ListParams.NO_LIMIT,
635 false /* useNetwork */);
636
637 // Build the new uri with this additional parameter
638 uri = uri
639 .buildUpon()
640 .appendQueryParameter(UIProvider.LIST_PARAMS_QUERY_PARAMETER,
641 listParams.serialize()).build();
642 setUri(uri);
643 }
644 }
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700645
646 @Override
647 protected ObjectCursor<ConversationMessage> getObjectCursor(Cursor inner) {
648 return new MessageCursor(inner);
649 }
mindypf4fce122012-09-14 15:55:33 -0700650 }
651
mindyp26d4d2d2012-09-18 17:30:32 -0700652 public abstract void onConversationUpdated(Conversation conversation);
653
Scott Kennedy18176782013-02-20 18:30:21 -0800654 public void onDetachedModeEntered() {
655 // If we have no messages, then we have nothing to display, so leave this view.
656 // Otherwise, just set the detached flag.
657 final Cursor messageCursor = getMessageCursor();
658
659 if (messageCursor == null || messageCursor.getCount() == 0) {
660 popOut();
661 } else {
662 mIsDetached = true;
663 }
664 }
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700665
666 /**
667 * Called when the JavaScript reports that it transformed a message.
668 * Sets a flag to true and invalidates the options menu so it will
669 * include the "Revert auto-sizing" menu option.
670 */
671 public void onConversationTransformed() {
672 mHasConversationBeenTransformed = true;
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700673 mHandler.post(new FragmentRunnable("invalidateOptionsMenu", this) {
Andrew Sappersteinae92e152013-05-03 13:55:18 -0700674 @Override
675 public void go() {
676 mActivity.invalidateOptionsMenu();
677 }
678 });
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700679 }
680
681 /**
682 * Called when the "Revert auto-sizing" option is selected. Default
683 * implementation simply sets a value on whether transforms should be
684 * applied. Derived classes should override this class and force a
685 * re-render so that the conversation renders without
686 */
687 public void showUntransformedConversation() {
688 // must set the value to true so we don't show the options menu item again
689 mHasConversationTransformBeenReverted = true;
690 }
691
692 /**
693 * Returns {@code true} if the conversation should be transformed. {@code false}, otherwise.
694 * @return {@code true} if the conversation should be transformed. {@code false}, otherwise.
695 */
696 public boolean shouldApplyTransforms() {
Alice Yang3617b412013-05-10 00:30:07 -0700697 return (mAccount.enableMessageTransforms > 0) &&
698 !mHasConversationTransformBeenReverted;
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700699 }
Andrew Sapperstein5c1692a2013-09-16 11:56:13 -0700700
James Lemieux7cad2802014-01-09 15:00:53 -0800701 /**
702 * The Print item in the overflow menu of the Conversation view is shown based on the return
703 * from this method.
704 *
705 * @return {@code true} if the conversation can be printed; {@code false} otherwise.
706 */
707 protected abstract boolean shouldShowPrintInOverflow();
708
709 /**
710 * Prints all messages in the conversation.
711 */
Andrew Sapperstein5c1692a2013-09-16 11:56:13 -0700712 protected abstract void printConversation();
Andrew Sapperstein8ec43e82013-12-17 18:27:55 -0800713
714 public boolean shouldAlwaysShowImages() {
715 return (mAccount != null) && (mAccount.settings.showImages == Settings.ShowImages.ALWAYS);
716 }
mindypf4fce122012-09-14 15:55:33 -0700717}