blob: bf2e9981ce32e4b99aac25e2971f6f7cc7512422 [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;
James Lemieux3f6111c2014-09-16 14:34:54 -070029import android.support.annotation.Nullable;
mindypf4fce122012-09-14 15:55:33 -070030import android.view.Menu;
31import android.view.MenuInflater;
32import android.view.MenuItem;
33
Tony Mantler821e5782014-01-06 15:33:43 -080034import com.android.emailcommon.mail.Address;
mindypf4fce122012-09-14 15:55:33 -070035import com.android.mail.R;
Andy Huang761522c2013-08-08 13:09:11 -070036import com.android.mail.analytics.Analytics;
Andy Huang8f187782012-11-06 17:49:25 -080037import com.android.mail.browse.ConversationAccountController;
Andrew Sapperstein8812d3c2013-06-04 17:06:41 -070038import com.android.mail.browse.ConversationMessage;
mindypf4fce122012-09-14 15:55:33 -070039import com.android.mail.browse.ConversationViewHeader.ConversationViewHeaderCallbacks;
Andy Huang9d3fd922012-09-26 22:23:58 -070040import com.android.mail.browse.MessageCursor;
mindypf4fce122012-09-14 15:55:33 -070041import com.android.mail.browse.MessageCursor.ConversationController;
Paul Westbrookc42ad5e2013-05-09 16:52:15 -070042import com.android.mail.content.ObjectCursor;
43import com.android.mail.content.ObjectCursorLoader;
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;
Andy Huang4f347e82014-02-25 17:32:28 -080070 public static final int ATTACHMENT_OPTION1_LOADER = 2;
mindypf4fce122012-09-14 15:55:33 -070071 protected ControllableActivity mActivity;
72 private final MessageLoaderCallbacks mMessageLoaderCallbacks = new MessageLoaderCallbacks();
Andrew Sapperstein376294b2013-06-06 16:04:26 -070073 private ContactLoaderCallbacks mContactLoaderCallbacks;
mindypf4fce122012-09-14 15:55:33 -070074 private MenuItem mChangeFoldersMenuItem;
75 protected Conversation mConversation;
mindypf4fce122012-09-14 15:55:33 -070076 protected String mBaseUri;
77 protected Account mAccount;
Andrew Sapperstein376294b2013-06-06 16:04:26 -070078
79 /**
80 * Must be instantiated in a derived class's onCreate.
81 */
82 protected AbstractConversationWebViewClient mWebViewClient;
Andrew Sapperstein4ddda2f2013-06-10 11:15:38 -070083
Andy Huang543e7092013-04-22 11:44:56 -070084 /**
85 * Cache of email address strings to parsed Address objects.
86 * <p>
87 * Remember to synchronize on the map when reading or writing to this cache, because some
88 * instances use it off the UI thread (e.g. from WebView).
89 */
90 protected final Map<String, Address> mAddressCache = Collections.synchronizedMap(
91 new HashMap<String, Address>());
mindypf4fce122012-09-14 15:55:33 -070092 private MessageCursor mCursor;
93 private Context mContext;
Andy Huang9d3fd922012-09-26 22:23:58 -070094 /**
95 * A backwards-compatible version of {{@link #getUserVisibleHint()}. Like the framework flag,
96 * this flag is saved and restored.
97 */
98 private boolean mUserVisible;
Andrew Sapperstein376294b2013-06-06 16:04:26 -070099
mindypff282d02012-09-17 10:33:02 -0700100 private final Handler mHandler = new Handler();
Vikram Aggarwald82a31f2013-02-05 15:03:00 -0800101 /** True if we want to avoid marking the conversation as viewed and read. */
102 private boolean mSuppressMarkingViewed;
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700103 /**
104 * Parcelable state of the conversation view. Can safely be used without null checking any time
Andy Huang9d3fd922012-09-26 22:23:58 -0700105 * after {@link #onCreate(Bundle)}.
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700106 */
107 protected ConversationViewState mViewState;
108
Scott Kennedy18176782013-02-20 18:30:21 -0800109 private boolean mIsDetached;
110
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700111 private boolean mHasConversationBeenTransformed;
112 private boolean mHasConversationTransformBeenReverted;
113
Andy Huange6c9fb62013-11-15 09:56:20 -0800114 protected boolean mConversationSeen = false;
115
mindypf4fce122012-09-14 15:55:33 -0700116 private final AccountObserver mAccountObserver = new AccountObserver() {
117 @Override
118 public void onChanged(Account newAccount) {
Andy Huangadbf3e82012-10-13 13:30:19 -0700119 final Account oldAccount = mAccount;
mindypf4fce122012-09-14 15:55:33 -0700120 mAccount = newAccount;
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700121 mWebViewClient.setAccount(mAccount);
Andy Huangadbf3e82012-10-13 13:30:19 -0700122 onAccountChanged(newAccount, oldAccount);
mindypf4fce122012-09-14 15:55:33 -0700123 }
124 };
125
Andy Huang9d3fd922012-09-26 22:23:58 -0700126 private static final String BUNDLE_VIEW_STATE =
127 AbstractConversationViewFragment.class.getName() + "viewstate";
Andy Huang9a8bc1e2012-10-23 19:48:25 -0700128 /**
129 * We save the user visible flag so the various transitions that occur during rotation do not
130 * cause unnecessary visibility change.
131 */
Andy Huang9d3fd922012-09-26 22:23:58 -0700132 private static final String BUNDLE_USER_VISIBLE =
133 AbstractConversationViewFragment.class.getName() + "uservisible";
134
Scott Kennedy18176782013-02-20 18:30:21 -0800135 private static final String BUNDLE_DETACHED =
136 AbstractConversationViewFragment.class.getName() + "detached";
137
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700138 private static final String BUNDLE_KEY_HAS_CONVERSATION_BEEN_TRANSFORMED =
139 AbstractConversationViewFragment.class.getName() + "conversationtransformed";
140 private static final String BUNDLE_KEY_HAS_CONVERSATION_BEEN_REVERTED =
141 AbstractConversationViewFragment.class.getName() + "conversationreverted";
142
Andrew Sapperstein606dbd72013-07-30 19:14:23 -0700143 public static Bundle makeBasicArgs(Account account) {
mindypf4fce122012-09-14 15:55:33 -0700144 Bundle args = new Bundle();
145 args.putParcelable(ARG_ACCOUNT, account);
mindypf4fce122012-09-14 15:55:33 -0700146 return args;
147 }
148
149 /**
150 * Constructor needs to be public to handle orientation changes and activity
151 * lifecycle events.
152 */
153 public AbstractConversationViewFragment() {
154 super();
155 }
156
157 /**
158 * Subclasses must override, since this depends on how many messages are
159 * shown in the conversation view.
160 */
Vikram Aggarwald82a31f2013-02-05 15:03:00 -0800161 protected void markUnread() {
162 // Do not automatically mark this conversation viewed and read.
163 mSuppressMarkingViewed = true;
164 }
mindypf4fce122012-09-14 15:55:33 -0700165
166 /**
167 * Subclasses must override this, since they may want to display a single or
168 * many messages related to this conversation.
169 */
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700170 protected abstract void onMessageCursorLoadFinished(
171 Loader<ObjectCursor<ConversationMessage>> loader,
Andy Huang014ea4c2012-09-25 14:50:54 -0700172 MessageCursor newCursor, MessageCursor oldCursor);
mindypf4fce122012-09-14 15:55:33 -0700173
174 /**
175 * Subclasses must override this, since they may want to display a single or
176 * many messages related to this conversation.
177 */
178 @Override
179 public abstract void onConversationViewHeaderHeightChange(int newHeight);
180
181 public abstract void onUserVisibleHintChanged();
182
183 /**
184 * Subclasses must override this.
185 */
Andy Huangadbf3e82012-10-13 13:30:19 -0700186 protected abstract void onAccountChanged(Account newAccount, Account oldAccount);
mindypf4fce122012-09-14 15:55:33 -0700187
188 @Override
189 public void onCreate(Bundle savedState) {
190 super.onCreate(savedState);
191
Andrew Sapperstein606dbd72013-07-30 19:14:23 -0700192 parseArguments();
193 setBaseUri();
Paul Westbrookba4cce62012-09-28 10:24:20 -0700194
mindypf4fce122012-09-14 15:55:33 -0700195 LogUtils.d(LOG_TAG, "onCreate in ConversationViewFragment (this=%s)", this);
196 // Not really, we just want to get a crack to store a reference to the change_folder item
197 setHasOptionsMenu(true);
Andy Huang9d3fd922012-09-26 22:23:58 -0700198
199 if (savedState != null) {
200 mViewState = savedState.getParcelable(BUNDLE_VIEW_STATE);
201 mUserVisible = savedState.getBoolean(BUNDLE_USER_VISIBLE);
Scott Kennedy18176782013-02-20 18:30:21 -0800202 mIsDetached = savedState.getBoolean(BUNDLE_DETACHED, false);
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700203 mHasConversationBeenTransformed =
204 savedState.getBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_TRANSFORMED, false);
205 mHasConversationTransformBeenReverted =
206 savedState.getBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_REVERTED, false);
Andy Huang9d3fd922012-09-26 22:23:58 -0700207 } else {
208 mViewState = getNewViewState();
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700209 mHasConversationBeenTransformed = false;
210 mHasConversationTransformBeenReverted = false;
Andy Huang9d3fd922012-09-26 22:23:58 -0700211 }
mindypf4fce122012-09-14 15:55:33 -0700212 }
213
Andrew Sapperstein606dbd72013-07-30 19:14:23 -0700214 /**
215 * Can be overridden in case a subclass needs to get additional arguments.
216 */
217 protected void parseArguments() {
218 final Bundle args = getArguments();
219 mAccount = args.getParcelable(ARG_ACCOUNT);
220 mConversation = args.getParcelable(ARG_CONVERSATION);
221 }
222
223 /**
224 * Can be overridden in case a subclass needs a different uri format
225 * (such as one that does not rely on account and/or conversation.
226 */
227 protected void setBaseUri() {
Ray Chenee04e662014-07-23 10:13:27 +0200228 mBaseUri = buildBaseUri(getContext(), mAccount, mConversation);
Andrew Sapperstein562c5ba2013-10-09 18:31:50 -0700229 }
230
Ray Chen4b0c0122014-07-11 15:24:54 +0200231 public static String buildBaseUri(Context context, Account account, Conversation conversation) {
Andrew Sapperstein606dbd72013-07-30 19:14:23 -0700232 // Since the uri specified in the conversation base uri may not be unique, we specify a
233 // base uri that us guaranteed to be unique for this conversation.
Paul Westbrookfd792372014-07-30 02:28:16 +0000234 return "x-thread://" + account.getAccountId().hashCode() + "/" + conversation.id;
Andrew Sapperstein606dbd72013-07-30 19:14:23 -0700235 }
236
Andy Huang9e4ca792013-02-28 14:33:43 -0800237 @Override
238 public String toString() {
239 // log extra info at DEBUG level or finer
240 final String s = super.toString();
241 if (!LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG) || mConversation == null) {
242 return s;
243 }
244 return "(" + s + " conv=" + mConversation + ")";
245 }
246
mindypf4fce122012-09-14 15:55:33 -0700247 @Override
248 public void onActivityCreated(Bundle savedInstanceState) {
249 super.onActivityCreated(savedInstanceState);
Vikram Aggarwal8fe8ed42012-09-18 11:40:08 -0700250 final Activity activity = getActivity();
mindypf4fce122012-09-14 15:55:33 -0700251 if (!(activity instanceof ControllableActivity)) {
252 LogUtils.wtf(LOG_TAG, "ConversationViewFragment expects only a ControllableActivity to"
253 + "create it. Cannot proceed.");
254 }
Vikram Aggarwal8fe8ed42012-09-18 11:40:08 -0700255 if (activity == null || activity.isFinishing()) {
mindypf4fce122012-09-14 15:55:33 -0700256 // Activity is finishing, just bail.
257 return;
258 }
Vikram Aggarwal8fe8ed42012-09-18 11:40:08 -0700259 mActivity = (ControllableActivity) activity;
mindypf4fce122012-09-14 15:55:33 -0700260 mContext = activity.getApplicationContext();
Andy Huangb622d2b2013-06-12 13:47:17 -0700261 mWebViewClient.setActivity(activity);
mindypf4fce122012-09-14 15:55:33 -0700262 mAccount = mAccountObserver.initialize(mActivity.getAccountController());
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700263 mWebViewClient.setAccount(mAccount);
mindypf4fce122012-09-14 15:55:33 -0700264 }
265
266 @Override
267 public ConversationUpdater getListController() {
268 final ControllableActivity activity = (ControllableActivity) getActivity();
269 return activity != null ? activity.getConversationUpdater() : null;
270 }
271
272 public Context getContext() {
273 return mContext;
274 }
275
Andy Huang02133aa2012-11-08 19:50:57 -0800276 @Override
mindypf4fce122012-09-14 15:55:33 -0700277 public Conversation getConversation() {
278 return mConversation;
279 }
280
281 @Override
James Lemieux3f6111c2014-09-16 14:34:54 -0700282 public @Nullable MessageCursor getMessageCursor() {
mindypf4fce122012-09-14 15:55:33 -0700283 return mCursor;
284 }
285
mindypff282d02012-09-17 10:33:02 -0700286 public Handler getHandler() {
287 return mHandler;
288 }
289
mindypf4fce122012-09-14 15:55:33 -0700290 public MessageLoaderCallbacks getMessageLoaderCallbacks() {
291 return mMessageLoaderCallbacks;
292 }
293
294 public ContactLoaderCallbacks getContactInfoSource() {
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700295 if (mContactLoaderCallbacks == null) {
Andrew Sapperstein8913ca62014-05-14 15:03:40 -0700296 mContactLoaderCallbacks = mActivity.getContactLoaderCallbacks();
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700297 }
mindypf4fce122012-09-14 15:55:33 -0700298 return mContactLoaderCallbacks;
299 }
300
301 @Override
302 public Account getAccount() {
303 return mAccount;
304 }
305
306 @Override
307 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
308 super.onCreateOptionsMenu(menu, inflater);
Andrew Sapperstein6c570db2013-08-06 17:21:36 -0700309 mChangeFoldersMenuItem = menu.findItem(R.id.change_folders);
mindypf4fce122012-09-14 15:55:33 -0700310 }
311
312 @Override
mindypf4fce122012-09-14 15:55:33 -0700313 public boolean onOptionsItemSelected(MenuItem item) {
Andy Huangbb9dd6b2013-02-28 17:13:54 -0800314 if (!isUserVisible()) {
315 // Unclear how this is happening. Current theory is that this fragment was scheduled
316 // to be removed, but the remove transaction failed. When the Activity is later
317 // restored, the FragmentManager restores this fragment, but Fragment.mMenuVisible is
318 // stuck at its initial value (true), which makes this zombie fragment eligible for
319 // menu item clicks.
320 //
321 // Work around this by relying on the (properly restored) extra user visible hint.
322 LogUtils.e(LOG_TAG,
323 "ACVF ignoring onOptionsItemSelected b/c userVisibleHint is false. f=%s", this);
324 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
Tony Mantler54a90572014-03-03 16:23:08 -0800325 LogUtils.e(LOG_TAG, "%s", Utils.dumpFragment(this));
Andy Huangbb9dd6b2013-02-28 17:13:54 -0800326 }
327 return false;
328 }
329
mindypf4fce122012-09-14 15:55:33 -0700330 boolean handled = false;
Scott Kennedy2b9d80e2013-07-30 23:03:45 -0700331 final int itemId = item.getItemId();
332 if (itemId == R.id.inside_conversation_unread) {
333 markUnread();
334 handled = true;
335 } else if (itemId == R.id.show_original) {
336 showUntransformedConversation();
337 handled = true;
Andrew Sapperstein6293ef02013-10-07 18:22:10 -0700338 } else if (itemId == R.id.print_all) {
Andrew Sapperstein5c1692a2013-09-16 11:56:13 -0700339 printConversation();
340 handled = true;
mindypf4fce122012-09-14 15:55:33 -0700341 }
342 return handled;
343 }
344
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700345 @Override
346 public void onPrepareOptionsMenu(Menu menu) {
347 // Only show option if we support message transforms and message has been transformed.
348 Utils.setMenuItemVisibility(menu, R.id.show_original, supportsMessageTransforms() &&
349 mHasConversationBeenTransformed && !mHasConversationTransformBeenReverted);
James Lemieux7cad2802014-01-09 15:00:53 -0800350
351 final MenuItem printMenuItem = menu.findItem(R.id.print_all);
352 if (printMenuItem != null) {
353 // compute the visibility of the print menu item
354 printMenuItem.setVisible(Utils.isRunningKitkatOrLater() && shouldShowPrintInOverflow());
355
356 // compute the text displayed on the print menu item
357 if (mConversation.getNumMessages() == 1) {
358 printMenuItem.setTitle(R.string.print);
359 } else {
360 printMenuItem.setTitle(R.string.print_all);
361 }
362 }
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700363 }
364
Andrew Sapperstein4ddda2f2013-06-10 11:15:38 -0700365 abstract boolean supportsMessageTransforms();
366
mindypf4fce122012-09-14 15:55:33 -0700367 // BEGIN conversation header callbacks
368 @Override
369 public void onFoldersClicked() {
370 if (mChangeFoldersMenuItem == null) {
371 LogUtils.e(LOG_TAG, "unable to open 'change folders' dialog for a conversation");
372 return;
373 }
374 mActivity.onOptionsItemSelected(mChangeFoldersMenuItem);
375 }
mindypf4fce122012-09-14 15:55:33 -0700376 // END conversation header callbacks
377
378 @Override
Andy Huang761522c2013-08-08 13:09:11 -0700379 public void onStart() {
380 super.onStart();
381
382 Analytics.getInstance().sendView(getClass().getName());
383 }
384
385 @Override
Andy Huang9d3fd922012-09-26 22:23:58 -0700386 public void onSaveInstanceState(Bundle outState) {
387 if (mViewState != null) {
388 outState.putParcelable(BUNDLE_VIEW_STATE, mViewState);
389 }
390 outState.putBoolean(BUNDLE_USER_VISIBLE, mUserVisible);
Scott Kennedy18176782013-02-20 18:30:21 -0800391 outState.putBoolean(BUNDLE_DETACHED, mIsDetached);
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700392 outState.putBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_TRANSFORMED,
393 mHasConversationBeenTransformed);
394 outState.putBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_REVERTED,
395 mHasConversationTransformBeenReverted);
Andy Huang9d3fd922012-09-26 22:23:58 -0700396 }
397
398 @Override
mindypf4fce122012-09-14 15:55:33 -0700399 public void onDestroyView() {
400 super.onDestroyView();
401 mAccountObserver.unregisterAndDestroy();
402 }
403
404 /**
405 * {@link #setUserVisibleHint(boolean)} only works on API >= 15, so implement our own for
406 * reliability on older platforms.
407 */
408 public void setExtraUserVisibleHint(boolean isVisibleToUser) {
409 LogUtils.v(LOG_TAG, "in CVF.setHint, val=%s (%s)", isVisibleToUser, this);
410 if (mUserVisible != isVisibleToUser) {
411 mUserVisible = isVisibleToUser;
mindypbc4142f2012-09-19 09:29:49 -0700412 MessageCursor cursor = getMessageCursor();
mindyp0b9b48c2012-09-19 10:00:51 -0700413 if (mUserVisible && (cursor != null && cursor.isLoaded() && cursor.getCount() == 0)) {
mindypbc4142f2012-09-19 09:29:49 -0700414 // Pop back to conversation list and show error.
415 onError();
416 return;
417 }
mindypf4fce122012-09-14 15:55:33 -0700418 onUserVisibleHintChanged();
419 }
420 }
421
Andy Huang9d3fd922012-09-26 22:23:58 -0700422 public boolean isUserVisible() {
423 return mUserVisible;
424 }
425
Andy Huang243c2362013-03-01 17:50:35 -0800426 protected void timerMark(String msg) {
427 if (isUserVisible()) {
428 Utils.sConvLoadTimer.mark(msg);
429 }
430 }
431
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700432 private class MessageLoaderCallbacks
433 implements LoaderManager.LoaderCallbacks<ObjectCursor<ConversationMessage>> {
mindypf4fce122012-09-14 15:55:33 -0700434
435 @Override
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700436 public Loader<ObjectCursor<ConversationMessage>> onCreateLoader(int id, Bundle args) {
Andy Huang02133aa2012-11-08 19:50:57 -0800437 return new MessageLoader(mActivity.getActivityContext(), mConversation.messageListUri);
mindypf4fce122012-09-14 15:55:33 -0700438 }
439
440 @Override
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700441 public void onLoadFinished(Loader<ObjectCursor<ConversationMessage>> loader,
442 ObjectCursor<ConversationMessage> data) {
mindypf4fce122012-09-14 15:55:33 -0700443 // ignore truly duplicate results
444 // this can happen when restoring after rotation
445 if (mCursor == data) {
446 return;
447 } else {
Andy Huang6766b6e2012-09-28 12:43:52 -0700448 final MessageCursor messageCursor = (MessageCursor) data;
mindypf4fce122012-09-14 15:55:33 -0700449
Andy Huang02133aa2012-11-08 19:50:57 -0800450 // bind the cursor to this fragment so it can access to the current list controller
451 messageCursor.setController(AbstractConversationViewFragment.this);
452
mindypf4fce122012-09-14 15:55:33 -0700453 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
454 LogUtils.d(LOG_TAG, "LOADED CONVERSATION= %s", messageCursor.getDebugDump());
455 }
456
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800457 // We have no messages: exit conversation view.
458 if (messageCursor.getCount() == 0
Scott Kennedy18176782013-02-20 18:30:21 -0800459 && (!CursorStatus.isWaitingForResults(messageCursor.getStatus())
460 || mIsDetached)) {
mindypf4fce122012-09-14 15:55:33 -0700461 if (mUserVisible) {
mindypbc4142f2012-09-19 09:29:49 -0700462 onError();
mindypf4fce122012-09-14 15:55:33 -0700463 } else {
464 // we expect that the pager adapter will remove this
465 // conversation fragment on its own due to a separate
466 // conversation cursor update (we might get here if the
467 // message list update fires first. nothing to do
468 // because we expect to be torn down soon.)
469 LogUtils.i(LOG_TAG, "CVF: offscreen conv has no messages, ignoring update"
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700470 + " in anticipation of conv cursor update. c=%s",
471 mConversation.uri);
mindypf4fce122012-09-14 15:55:33 -0700472 }
Andy Huang233d4352012-10-18 14:00:24 -0700473 // existing mCursor will imminently be closed, must stop referencing it
474 // since we expect to be kicked out soon, it doesn't matter what mCursor
475 // becomes
476 mCursor = null;
mindypf4fce122012-09-14 15:55:33 -0700477 return;
478 }
479
480 // ignore cursors that are still loading results
481 if (!messageCursor.isLoaded()) {
Andy Huang233d4352012-10-18 14:00:24 -0700482 // existing mCursor will imminently be closed, must stop referencing it
483 // in this case, the new cursor is also no good, and since don't expect to get
484 // here except in initial load situations, it's safest to just ensure the
485 // reference is null
486 mCursor = null;
mindypf4fce122012-09-14 15:55:33 -0700487 return;
488 }
Andy Huang014ea4c2012-09-25 14:50:54 -0700489 final MessageCursor oldCursor = mCursor;
Andy Huang6766b6e2012-09-28 12:43:52 -0700490 mCursor = messageCursor;
Andy Huang014ea4c2012-09-25 14:50:54 -0700491 onMessageCursorLoadFinished(loader, mCursor, oldCursor);
mindypf4fce122012-09-14 15:55:33 -0700492 }
493 }
494
495 @Override
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700496 public void onLoaderReset(Loader<ObjectCursor<ConversationMessage>> loader) {
mindypf4fce122012-09-14 15:55:33 -0700497 mCursor = null;
498 }
499
500 }
501
mindypbc4142f2012-09-19 09:29:49 -0700502 private void onError() {
503 // need to exit this view- conversation may have been
504 // deleted, or for whatever reason is now invalid (e.g.
505 // discard single draft)
506 //
507 // N.B. this may involve a fragment transaction, which
508 // FragmentManager will refuse to execute directly
509 // within onLoadFinished. Make sure the controller knows.
510 LogUtils.i(LOG_TAG, "CVF: visible conv has no messages, exiting conv mode");
511 // TODO(mindyp): handle ERROR status by showing an error
512 // message to the user that there are no messages in
513 // this conversation
Scott Kennedy18176782013-02-20 18:30:21 -0800514 popOut();
515 }
mindypbc4142f2012-09-19 09:29:49 -0700516
Scott Kennedy18176782013-02-20 18:30:21 -0800517 private void popOut() {
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700518 mHandler.post(new FragmentRunnable("popOut", this) {
mindypbc4142f2012-09-19 09:29:49 -0700519 @Override
Andy Huang9a8bc1e2012-10-23 19:48:25 -0700520 public void go() {
Alice Yang7729a9a2013-05-23 18:09:09 -0700521 if (mActivity != null) {
522 mActivity.getListHandler()
523 .onConversationSelected(null, true /* inLoaderCallbacks */);
524 }
mindypbc4142f2012-09-19 09:29:49 -0700525 }
mindypbc4142f2012-09-19 09:29:49 -0700526 });
527 }
528
Andy Huange6c9fb62013-11-15 09:56:20 -0800529 /**
530 * @see Folder#getTypeDescription()
531 */
532 protected String getCurrentFolderTypeDesc() {
533 final Folder currFolder;
534 if (mActivity != null) {
535 currFolder = mActivity.getFolderController().getFolder();
536 } else {
537 currFolder = null;
538 }
539 final String folderStr;
540 if (currFolder != null) {
541 folderStr = currFolder.getTypeDescription();
542 } else {
543 folderStr = "unknown_folder";
544 }
545 return folderStr;
546 }
547
548 private void logConversationView() {
549 final String folderStr = getCurrentFolderTypeDesc();
550 Analytics.getInstance().sendEvent("view_conversation", folderStr,
551 mConversation.isRemote ? "unsynced" : "synced", mConversation.getNumMessages());
552 }
553
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700554 protected void onConversationSeen() {
Scott Kennedy919d01a2013-05-07 16:13:29 -0700555 LogUtils.d(LOG_TAG, "AbstractConversationViewFragment#onConversationSeen()");
556
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700557 // Ignore unsafe calls made after a fragment is detached from an activity
558 final ControllableActivity activity = (ControllableActivity) getActivity();
559 if (activity == null) {
560 LogUtils.w(LOG_TAG, "ignoring onConversationSeen for conv=%s", mConversation.id);
561 return;
562 }
563
Andy Huange6c9fb62013-11-15 09:56:20 -0800564 // this method is called 2x on rotation; debounce this a bit so as not to
565 // dramatically skew analytics data too much. Ideally, it should be called zero times
566 // on rotation...
567 if (!mConversationSeen) {
568 logConversationView();
569 }
570
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700571 mViewState.setInfoForConversation(mConversation);
572
Scott Kennedy919d01a2013-05-07 16:13:29 -0700573 LogUtils.d(LOG_TAG, "onConversationSeen() - mSuppressMarkingViewed = %b",
574 mSuppressMarkingViewed);
Vikram Aggarwald82a31f2013-02-05 15:03:00 -0800575 // In most circumstances we want to mark the conversation as viewed and read, since the
576 // user has read it. However, if the user has already marked the conversation unread, we
577 // do not want a later mark-read operation to undo this. So we check this variable which
578 // is set in #markUnread() which suppresses automatic mark-read.
579 if (!mSuppressMarkingViewed) {
580 // mark viewed/read if not previously marked viewed by this conversation view,
581 // or if unread messages still exist in the message list cursor
582 // we don't want to keep marking viewed on rotation or restore
583 // but we do want future re-renders to mark read (e.g. "New message from X" case)
Paul Westbrook21452292013-04-15 18:51:07 -0700584 final MessageCursor cursor = getMessageCursor();
Scott Kennedy919d01a2013-05-07 16:13:29 -0700585 LogUtils.d(LOG_TAG, "onConversationSeen() - mConversation.isViewed() = %b, "
586 + "cursor null = %b, cursor.isConversationRead() = %b",
587 mConversation.isViewed(), cursor == null,
588 cursor != null && cursor.isConversationRead());
Vikram Aggarwald82a31f2013-02-05 15:03:00 -0800589 if (!mConversation.isViewed() || (cursor != null && !cursor.isConversationRead())) {
590 // Mark the conversation viewed and read.
591 activity.getConversationUpdater()
592 .markConversationsRead(Arrays.asList(mConversation), true, true);
Yu Ping Hu7c909c72013-01-18 11:58:01 -0800593
Vikram Aggarwald82a31f2013-02-05 15:03:00 -0800594 // and update the Message objects in the cursor so the next time a cursor update
595 // happens with these messages marked read, we know to ignore it
Paul Westbrook21452292013-04-15 18:51:07 -0700596 if (cursor != null && !cursor.isClosed()) {
Vikram Aggarwald82a31f2013-02-05 15:03:00 -0800597 cursor.markMessagesRead();
598 }
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700599 }
600 }
Scott Kennedy3b965d72013-06-25 14:36:55 -0700601 activity.getListHandler().onConversationSeen();
Andy Huange6c9fb62013-11-15 09:56:20 -0800602
603 mConversationSeen = true;
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700604 }
605
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700606 protected ConversationViewState getNewViewState() {
607 return new ConversationViewState();
608 }
609
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700610 private static class MessageLoader extends ObjectCursorLoader<ConversationMessage> {
mindypf4fce122012-09-14 15:55:33 -0700611 private boolean mDeliveredFirstResults = false;
mindypf4fce122012-09-14 15:55:33 -0700612
Andy Huang02133aa2012-11-08 19:50:57 -0800613 public MessageLoader(Context c, Uri messageListUri) {
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700614 super(c, messageListUri, UIProvider.MESSAGE_PROJECTION, ConversationMessage.FACTORY);
mindypf4fce122012-09-14 15:55:33 -0700615 }
616
617 @Override
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700618 public void deliverResult(ObjectCursor<ConversationMessage> result) {
mindypf4fce122012-09-14 15:55:33 -0700619 // We want to deliver these results, and then we want to make sure
620 // that any subsequent
621 // queries do not hit the network
622 super.deliverResult(result);
623
624 if (!mDeliveredFirstResults) {
625 mDeliveredFirstResults = true;
626 Uri uri = getUri();
627
628 // Create a ListParams that tells the provider to not hit the
629 // network
630 final ListParams listParams = new ListParams(ListParams.NO_LIMIT,
631 false /* useNetwork */);
632
633 // Build the new uri with this additional parameter
634 uri = uri
635 .buildUpon()
636 .appendQueryParameter(UIProvider.LIST_PARAMS_QUERY_PARAMETER,
637 listParams.serialize()).build();
638 setUri(uri);
639 }
640 }
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700641
642 @Override
643 protected ObjectCursor<ConversationMessage> getObjectCursor(Cursor inner) {
644 return new MessageCursor(inner);
645 }
mindypf4fce122012-09-14 15:55:33 -0700646 }
647
mindyp26d4d2d2012-09-18 17:30:32 -0700648 public abstract void onConversationUpdated(Conversation conversation);
649
Scott Kennedy18176782013-02-20 18:30:21 -0800650 public void onDetachedModeEntered() {
651 // If we have no messages, then we have nothing to display, so leave this view.
652 // Otherwise, just set the detached flag.
653 final Cursor messageCursor = getMessageCursor();
654
655 if (messageCursor == null || messageCursor.getCount() == 0) {
656 popOut();
657 } else {
658 mIsDetached = true;
659 }
660 }
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700661
662 /**
663 * Called when the JavaScript reports that it transformed a message.
664 * Sets a flag to true and invalidates the options menu so it will
665 * include the "Revert auto-sizing" menu option.
666 */
667 public void onConversationTransformed() {
668 mHasConversationBeenTransformed = true;
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700669 mHandler.post(new FragmentRunnable("invalidateOptionsMenu", this) {
Andrew Sappersteinae92e152013-05-03 13:55:18 -0700670 @Override
671 public void go() {
Andrew Sapperstein52882ff2014-07-27 12:30:18 -0700672 mActivity.supportInvalidateOptionsMenu();
Andrew Sappersteinae92e152013-05-03 13:55:18 -0700673 }
674 });
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700675 }
676
677 /**
678 * Called when the "Revert auto-sizing" option is selected. Default
679 * implementation simply sets a value on whether transforms should be
680 * applied. Derived classes should override this class and force a
681 * re-render so that the conversation renders without
682 */
683 public void showUntransformedConversation() {
684 // must set the value to true so we don't show the options menu item again
685 mHasConversationTransformBeenReverted = true;
686 }
687
688 /**
689 * Returns {@code true} if the conversation should be transformed. {@code false}, otherwise.
690 * @return {@code true} if the conversation should be transformed. {@code false}, otherwise.
691 */
692 public boolean shouldApplyTransforms() {
Alice Yang3617b412013-05-10 00:30:07 -0700693 return (mAccount.enableMessageTransforms > 0) &&
694 !mHasConversationTransformBeenReverted;
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700695 }
Andrew Sapperstein5c1692a2013-09-16 11:56:13 -0700696
James Lemieux7cad2802014-01-09 15:00:53 -0800697 /**
698 * The Print item in the overflow menu of the Conversation view is shown based on the return
699 * from this method.
700 *
701 * @return {@code true} if the conversation can be printed; {@code false} otherwise.
702 */
703 protected abstract boolean shouldShowPrintInOverflow();
704
705 /**
706 * Prints all messages in the conversation.
707 */
Andrew Sapperstein5c1692a2013-09-16 11:56:13 -0700708 protected abstract void printConversation();
Andrew Sapperstein8ec43e82013-12-17 18:27:55 -0800709
710 public boolean shouldAlwaysShowImages() {
711 return (mAccount != null) && (mAccount.settings.showImages == Settings.ShowImages.ALWAYS);
712 }
mindypf4fce122012-09-14 15:55:33 -0700713}