blob: e9a1951f4c3638406ba9fd57bb3f25c9efa279f2 [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
mindypf4fce122012-09-14 15:55:33 -070033import com.android.mail.R;
Andy Huang8f187782012-11-06 17:49:25 -080034import com.android.mail.browse.ConversationAccountController;
Andrew Sapperstein8812d3c2013-06-04 17:06:41 -070035import com.android.mail.browse.ConversationMessage;
mindypf4fce122012-09-14 15:55:33 -070036import com.android.mail.browse.ConversationViewHeader.ConversationViewHeaderCallbacks;
Andy Huang9d3fd922012-09-26 22:23:58 -070037import com.android.mail.browse.MessageCursor;
mindypf4fce122012-09-14 15:55:33 -070038import com.android.mail.browse.MessageCursor.ConversationController;
Paul Westbrookc42ad5e2013-05-09 16:52:15 -070039import com.android.mail.content.ObjectCursor;
40import com.android.mail.content.ObjectCursorLoader;
mindypf4fce122012-09-14 15:55:33 -070041import com.android.mail.providers.Account;
42import com.android.mail.providers.AccountObserver;
43import com.android.mail.providers.Address;
44import com.android.mail.providers.Conversation;
45import com.android.mail.providers.Folder;
46import com.android.mail.providers.ListParams;
47import com.android.mail.providers.UIProvider;
Vikram Aggarwala91d00b2013-01-18 12:00:37 -080048import com.android.mail.providers.UIProvider.CursorStatus;
mindypf4fce122012-09-14 15:55:33 -070049import com.android.mail.utils.LogTag;
50import com.android.mail.utils.LogUtils;
51import com.android.mail.utils.Utils;
mindypf4fce122012-09-14 15:55:33 -070052
Paul Westbrook4d8cad52012-09-21 14:13:49 -070053import java.util.Arrays;
Andy Huang543e7092013-04-22 11:44:56 -070054import java.util.Collections;
55import java.util.HashMap;
mindypf4fce122012-09-14 15:55:33 -070056import java.util.Map;
Andrew Sapperstein376294b2013-06-06 16:04:26 -070057
mindypf4fce122012-09-14 15:55:33 -070058
59public abstract class AbstractConversationViewFragment extends Fragment implements
Andrew Sapperstein4ddda2f2013-06-10 11:15:38 -070060 ConversationController, ConversationAccountController,
mindypf4fce122012-09-14 15:55:33 -070061 ConversationViewHeaderCallbacks {
62
63 private static final String ARG_ACCOUNT = "account";
64 public static final String ARG_CONVERSATION = "conversation";
65 private static final String ARG_FOLDER = "folder";
66 private static final String LOG_TAG = LogTag.getLogTag();
67 protected static final int MESSAGE_LOADER = 0;
68 protected static final int CONTACT_LOADER = 1;
69 protected ControllableActivity mActivity;
70 private final MessageLoaderCallbacks mMessageLoaderCallbacks = new MessageLoaderCallbacks();
Andrew Sapperstein376294b2013-06-06 16:04:26 -070071 private ContactLoaderCallbacks mContactLoaderCallbacks;
mindypf4fce122012-09-14 15:55:33 -070072 private MenuItem mChangeFoldersMenuItem;
73 protected Conversation mConversation;
74 protected Folder mFolder;
75 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
mindypf4fce122012-09-14 15:55:33 -0700113 private final AccountObserver mAccountObserver = new AccountObserver() {
114 @Override
115 public void onChanged(Account newAccount) {
Andy Huangadbf3e82012-10-13 13:30:19 -0700116 final Account oldAccount = mAccount;
mindypf4fce122012-09-14 15:55:33 -0700117 mAccount = newAccount;
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700118 mWebViewClient.setAccount(mAccount);
Andy Huangadbf3e82012-10-13 13:30:19 -0700119 onAccountChanged(newAccount, oldAccount);
mindypf4fce122012-09-14 15:55:33 -0700120 }
121 };
122
Andy Huang9d3fd922012-09-26 22:23:58 -0700123 private static final String BUNDLE_VIEW_STATE =
124 AbstractConversationViewFragment.class.getName() + "viewstate";
Andy Huang9a8bc1e2012-10-23 19:48:25 -0700125 /**
126 * We save the user visible flag so the various transitions that occur during rotation do not
127 * cause unnecessary visibility change.
128 */
Andy Huang9d3fd922012-09-26 22:23:58 -0700129 private static final String BUNDLE_USER_VISIBLE =
130 AbstractConversationViewFragment.class.getName() + "uservisible";
131
Scott Kennedy18176782013-02-20 18:30:21 -0800132 private static final String BUNDLE_DETACHED =
133 AbstractConversationViewFragment.class.getName() + "detached";
134
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700135 private static final String BUNDLE_KEY_HAS_CONVERSATION_BEEN_TRANSFORMED =
136 AbstractConversationViewFragment.class.getName() + "conversationtransformed";
137 private static final String BUNDLE_KEY_HAS_CONVERSATION_BEEN_REVERTED =
138 AbstractConversationViewFragment.class.getName() + "conversationreverted";
139
mindypf4fce122012-09-14 15:55:33 -0700140 public static Bundle makeBasicArgs(Account account, Folder folder) {
141 Bundle args = new Bundle();
142 args.putParcelable(ARG_ACCOUNT, account);
143 args.putParcelable(ARG_FOLDER, folder);
144 return args;
145 }
146
147 /**
148 * Constructor needs to be public to handle orientation changes and activity
149 * lifecycle events.
150 */
151 public AbstractConversationViewFragment() {
152 super();
153 }
154
155 /**
156 * Subclasses must override, since this depends on how many messages are
157 * shown in the conversation view.
158 */
Vikram Aggarwald82a31f2013-02-05 15:03:00 -0800159 protected void markUnread() {
160 // Do not automatically mark this conversation viewed and read.
161 mSuppressMarkingViewed = true;
162 }
mindypf4fce122012-09-14 15:55:33 -0700163
164 /**
165 * Subclasses must override this, since they may want to display a single or
166 * many messages related to this conversation.
167 */
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700168 protected abstract void onMessageCursorLoadFinished(
169 Loader<ObjectCursor<ConversationMessage>> loader,
Andy Huang014ea4c2012-09-25 14:50:54 -0700170 MessageCursor newCursor, MessageCursor oldCursor);
mindypf4fce122012-09-14 15:55:33 -0700171
172 /**
173 * Subclasses must override this, since they may want to display a single or
174 * many messages related to this conversation.
175 */
176 @Override
177 public abstract void onConversationViewHeaderHeightChange(int newHeight);
178
179 public abstract void onUserVisibleHintChanged();
180
181 /**
182 * Subclasses must override this.
183 */
Andy Huangadbf3e82012-10-13 13:30:19 -0700184 protected abstract void onAccountChanged(Account newAccount, Account oldAccount);
mindypf4fce122012-09-14 15:55:33 -0700185
186 @Override
187 public void onCreate(Bundle savedState) {
188 super.onCreate(savedState);
189
190 final Bundle args = getArguments();
191 mAccount = args.getParcelable(ARG_ACCOUNT);
192 mConversation = args.getParcelable(ARG_CONVERSATION);
193 mFolder = args.getParcelable(ARG_FOLDER);
Paul Westbrookba4cce62012-09-28 10:24:20 -0700194
195 // Since the uri specified in the conversation base uri may not be unique, we specify a
196 // base uri that us guaranteed to be unique for this conversation.
197 mBaseUri = "x-thread://" + mAccount.name + "/" + mConversation.id;
198
mindypf4fce122012-09-14 15:55:33 -0700199 LogUtils.d(LOG_TAG, "onCreate in ConversationViewFragment (this=%s)", this);
200 // Not really, we just want to get a crack to store a reference to the change_folder item
201 setHasOptionsMenu(true);
Andy Huang9d3fd922012-09-26 22:23:58 -0700202
203 if (savedState != null) {
204 mViewState = savedState.getParcelable(BUNDLE_VIEW_STATE);
205 mUserVisible = savedState.getBoolean(BUNDLE_USER_VISIBLE);
Scott Kennedy18176782013-02-20 18:30:21 -0800206 mIsDetached = savedState.getBoolean(BUNDLE_DETACHED, false);
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700207 mHasConversationBeenTransformed =
208 savedState.getBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_TRANSFORMED, false);
209 mHasConversationTransformBeenReverted =
210 savedState.getBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_REVERTED, false);
Andy Huang9d3fd922012-09-26 22:23:58 -0700211 } else {
212 mViewState = getNewViewState();
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700213 mHasConversationBeenTransformed = false;
214 mHasConversationTransformBeenReverted = false;
Andy Huang9d3fd922012-09-26 22:23:58 -0700215 }
mindypf4fce122012-09-14 15:55:33 -0700216 }
217
Andy Huang9e4ca792013-02-28 14:33:43 -0800218 @Override
219 public String toString() {
220 // log extra info at DEBUG level or finer
221 final String s = super.toString();
222 if (!LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG) || mConversation == null) {
223 return s;
224 }
225 return "(" + s + " conv=" + mConversation + ")";
226 }
227
mindypf4fce122012-09-14 15:55:33 -0700228 @Override
229 public void onActivityCreated(Bundle savedInstanceState) {
230 super.onActivityCreated(savedInstanceState);
Vikram Aggarwal8fe8ed42012-09-18 11:40:08 -0700231 final Activity activity = getActivity();
mindypf4fce122012-09-14 15:55:33 -0700232 if (!(activity instanceof ControllableActivity)) {
233 LogUtils.wtf(LOG_TAG, "ConversationViewFragment expects only a ControllableActivity to"
234 + "create it. Cannot proceed.");
235 }
Vikram Aggarwal8fe8ed42012-09-18 11:40:08 -0700236 if (activity == null || activity.isFinishing()) {
mindypf4fce122012-09-14 15:55:33 -0700237 // Activity is finishing, just bail.
238 return;
239 }
Vikram Aggarwal8fe8ed42012-09-18 11:40:08 -0700240 mActivity = (ControllableActivity) activity;
mindypf4fce122012-09-14 15:55:33 -0700241 mContext = activity.getApplicationContext();
Andy Huangb622d2b2013-06-12 13:47:17 -0700242 mWebViewClient.setActivity(activity);
mindypf4fce122012-09-14 15:55:33 -0700243 mAccount = mAccountObserver.initialize(mActivity.getAccountController());
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700244 mWebViewClient.setAccount(mAccount);
mindypf4fce122012-09-14 15:55:33 -0700245 }
246
247 @Override
248 public ConversationUpdater getListController() {
249 final ControllableActivity activity = (ControllableActivity) getActivity();
250 return activity != null ? activity.getConversationUpdater() : null;
251 }
252
253 public Context getContext() {
254 return mContext;
255 }
256
Andy Huang02133aa2012-11-08 19:50:57 -0800257 @Override
mindypf4fce122012-09-14 15:55:33 -0700258 public Conversation getConversation() {
259 return mConversation;
260 }
261
262 @Override
263 public MessageCursor getMessageCursor() {
264 return mCursor;
265 }
266
mindypff282d02012-09-17 10:33:02 -0700267 public Handler getHandler() {
268 return mHandler;
269 }
270
mindypf4fce122012-09-14 15:55:33 -0700271 public MessageLoaderCallbacks getMessageLoaderCallbacks() {
272 return mMessageLoaderCallbacks;
273 }
274
275 public ContactLoaderCallbacks getContactInfoSource() {
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700276 if (mContactLoaderCallbacks == null) {
277 mContactLoaderCallbacks = new ContactLoaderCallbacks(mActivity.getActivityContext());
278 }
mindypf4fce122012-09-14 15:55:33 -0700279 return mContactLoaderCallbacks;
280 }
281
282 @Override
283 public Account getAccount() {
284 return mAccount;
285 }
286
287 @Override
288 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
289 super.onCreateOptionsMenu(menu, inflater);
290 mChangeFoldersMenuItem = menu.findItem(R.id.change_folder);
291 }
292
293 @Override
mindypf4fce122012-09-14 15:55:33 -0700294 public boolean onOptionsItemSelected(MenuItem item) {
Andy Huangbb9dd6b2013-02-28 17:13:54 -0800295 if (!isUserVisible()) {
296 // Unclear how this is happening. Current theory is that this fragment was scheduled
297 // to be removed, but the remove transaction failed. When the Activity is later
298 // restored, the FragmentManager restores this fragment, but Fragment.mMenuVisible is
299 // stuck at its initial value (true), which makes this zombie fragment eligible for
300 // menu item clicks.
301 //
302 // Work around this by relying on the (properly restored) extra user visible hint.
303 LogUtils.e(LOG_TAG,
304 "ACVF ignoring onOptionsItemSelected b/c userVisibleHint is false. f=%s", this);
305 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
Scott Kennedyb184bfe2013-05-25 21:29:22 -0700306 LogUtils.e(LOG_TAG, Utils.dumpFragment(this)); // the dump has '%' chars in it...
Andy Huangbb9dd6b2013-02-28 17:13:54 -0800307 }
308 return false;
309 }
310
mindypf4fce122012-09-14 15:55:33 -0700311 boolean handled = false;
312 switch (item.getItemId()) {
313 case R.id.inside_conversation_unread:
314 markUnread();
315 handled = true;
316 break;
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700317 case R.id.show_original:
318 showUntransformedConversation();
319 handled = true;
320 break;
mindypf4fce122012-09-14 15:55:33 -0700321 }
322 return handled;
323 }
324
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700325 @Override
326 public void onPrepareOptionsMenu(Menu menu) {
327 // Only show option if we support message transforms and message has been transformed.
328 Utils.setMenuItemVisibility(menu, R.id.show_original, supportsMessageTransforms() &&
329 mHasConversationBeenTransformed && !mHasConversationTransformBeenReverted);
330 }
331
Andrew Sapperstein4ddda2f2013-06-10 11:15:38 -0700332 abstract boolean supportsMessageTransforms();
333
mindypf4fce122012-09-14 15:55:33 -0700334 // BEGIN conversation header callbacks
335 @Override
336 public void onFoldersClicked() {
337 if (mChangeFoldersMenuItem == null) {
338 LogUtils.e(LOG_TAG, "unable to open 'change folders' dialog for a conversation");
339 return;
340 }
341 mActivity.onOptionsItemSelected(mChangeFoldersMenuItem);
342 }
mindypf4fce122012-09-14 15:55:33 -0700343 // END conversation header callbacks
344
345 @Override
Andy Huang9d3fd922012-09-26 22:23:58 -0700346 public void onSaveInstanceState(Bundle outState) {
347 if (mViewState != null) {
348 outState.putParcelable(BUNDLE_VIEW_STATE, mViewState);
349 }
350 outState.putBoolean(BUNDLE_USER_VISIBLE, mUserVisible);
Scott Kennedy18176782013-02-20 18:30:21 -0800351 outState.putBoolean(BUNDLE_DETACHED, mIsDetached);
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700352 outState.putBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_TRANSFORMED,
353 mHasConversationBeenTransformed);
354 outState.putBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_REVERTED,
355 mHasConversationTransformBeenReverted);
Andy Huang9d3fd922012-09-26 22:23:58 -0700356 }
357
358 @Override
mindypf4fce122012-09-14 15:55:33 -0700359 public void onDestroyView() {
360 super.onDestroyView();
361 mAccountObserver.unregisterAndDestroy();
362 }
363
364 /**
365 * {@link #setUserVisibleHint(boolean)} only works on API >= 15, so implement our own for
366 * reliability on older platforms.
367 */
368 public void setExtraUserVisibleHint(boolean isVisibleToUser) {
369 LogUtils.v(LOG_TAG, "in CVF.setHint, val=%s (%s)", isVisibleToUser, this);
370 if (mUserVisible != isVisibleToUser) {
371 mUserVisible = isVisibleToUser;
mindypbc4142f2012-09-19 09:29:49 -0700372 MessageCursor cursor = getMessageCursor();
mindyp0b9b48c2012-09-19 10:00:51 -0700373 if (mUserVisible && (cursor != null && cursor.isLoaded() && cursor.getCount() == 0)) {
mindypbc4142f2012-09-19 09:29:49 -0700374 // Pop back to conversation list and show error.
375 onError();
376 return;
377 }
mindypf4fce122012-09-14 15:55:33 -0700378 onUserVisibleHintChanged();
379 }
380 }
381
Andy Huang9d3fd922012-09-26 22:23:58 -0700382 public boolean isUserVisible() {
383 return mUserVisible;
384 }
385
Andy Huang243c2362013-03-01 17:50:35 -0800386 protected void timerMark(String msg) {
387 if (isUserVisible()) {
388 Utils.sConvLoadTimer.mark(msg);
389 }
390 }
391
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700392 private class MessageLoaderCallbacks
393 implements LoaderManager.LoaderCallbacks<ObjectCursor<ConversationMessage>> {
mindypf4fce122012-09-14 15:55:33 -0700394
395 @Override
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700396 public Loader<ObjectCursor<ConversationMessage>> onCreateLoader(int id, Bundle args) {
Andy Huang02133aa2012-11-08 19:50:57 -0800397 return new MessageLoader(mActivity.getActivityContext(), mConversation.messageListUri);
mindypf4fce122012-09-14 15:55:33 -0700398 }
399
400 @Override
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700401 public void onLoadFinished(Loader<ObjectCursor<ConversationMessage>> loader,
402 ObjectCursor<ConversationMessage> data) {
mindypf4fce122012-09-14 15:55:33 -0700403 // ignore truly duplicate results
404 // this can happen when restoring after rotation
405 if (mCursor == data) {
406 return;
407 } else {
Andy Huang6766b6e2012-09-28 12:43:52 -0700408 final MessageCursor messageCursor = (MessageCursor) data;
mindypf4fce122012-09-14 15:55:33 -0700409
Andy Huang02133aa2012-11-08 19:50:57 -0800410 // bind the cursor to this fragment so it can access to the current list controller
411 messageCursor.setController(AbstractConversationViewFragment.this);
412
mindypf4fce122012-09-14 15:55:33 -0700413 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
414 LogUtils.d(LOG_TAG, "LOADED CONVERSATION= %s", messageCursor.getDebugDump());
415 }
416
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800417 // We have no messages: exit conversation view.
418 if (messageCursor.getCount() == 0
Scott Kennedy18176782013-02-20 18:30:21 -0800419 && (!CursorStatus.isWaitingForResults(messageCursor.getStatus())
420 || mIsDetached)) {
mindypf4fce122012-09-14 15:55:33 -0700421 if (mUserVisible) {
mindypbc4142f2012-09-19 09:29:49 -0700422 onError();
mindypf4fce122012-09-14 15:55:33 -0700423 } else {
424 // we expect that the pager adapter will remove this
425 // conversation fragment on its own due to a separate
426 // conversation cursor update (we might get here if the
427 // message list update fires first. nothing to do
428 // because we expect to be torn down soon.)
429 LogUtils.i(LOG_TAG, "CVF: offscreen conv has no messages, ignoring update"
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700430 + " in anticipation of conv cursor update. c=%s",
431 mConversation.uri);
mindypf4fce122012-09-14 15:55:33 -0700432 }
Andy Huang233d4352012-10-18 14:00:24 -0700433 // existing mCursor will imminently be closed, must stop referencing it
434 // since we expect to be kicked out soon, it doesn't matter what mCursor
435 // becomes
436 mCursor = null;
mindypf4fce122012-09-14 15:55:33 -0700437 return;
438 }
439
440 // ignore cursors that are still loading results
441 if (!messageCursor.isLoaded()) {
Andy Huang233d4352012-10-18 14:00:24 -0700442 // existing mCursor will imminently be closed, must stop referencing it
443 // in this case, the new cursor is also no good, and since don't expect to get
444 // here except in initial load situations, it's safest to just ensure the
445 // reference is null
446 mCursor = null;
mindypf4fce122012-09-14 15:55:33 -0700447 return;
448 }
Andy Huang014ea4c2012-09-25 14:50:54 -0700449 final MessageCursor oldCursor = mCursor;
Andy Huang6766b6e2012-09-28 12:43:52 -0700450 mCursor = messageCursor;
Andy Huang014ea4c2012-09-25 14:50:54 -0700451 onMessageCursorLoadFinished(loader, mCursor, oldCursor);
mindypf4fce122012-09-14 15:55:33 -0700452 }
453 }
454
455 @Override
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700456 public void onLoaderReset(Loader<ObjectCursor<ConversationMessage>> loader) {
mindypf4fce122012-09-14 15:55:33 -0700457 mCursor = null;
458 }
459
460 }
461
mindypbc4142f2012-09-19 09:29:49 -0700462 private void onError() {
463 // need to exit this view- conversation may have been
464 // deleted, or for whatever reason is now invalid (e.g.
465 // discard single draft)
466 //
467 // N.B. this may involve a fragment transaction, which
468 // FragmentManager will refuse to execute directly
469 // within onLoadFinished. Make sure the controller knows.
470 LogUtils.i(LOG_TAG, "CVF: visible conv has no messages, exiting conv mode");
471 // TODO(mindyp): handle ERROR status by showing an error
472 // message to the user that there are no messages in
473 // this conversation
Scott Kennedy18176782013-02-20 18:30:21 -0800474 popOut();
475 }
mindypbc4142f2012-09-19 09:29:49 -0700476
Scott Kennedy18176782013-02-20 18:30:21 -0800477 private void popOut() {
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700478 mHandler.post(new FragmentRunnable("popOut", this) {
mindypbc4142f2012-09-19 09:29:49 -0700479 @Override
Andy Huang9a8bc1e2012-10-23 19:48:25 -0700480 public void go() {
Alice Yang7729a9a2013-05-23 18:09:09 -0700481 if (mActivity != null) {
482 mActivity.getListHandler()
483 .onConversationSelected(null, true /* inLoaderCallbacks */);
484 }
mindypbc4142f2012-09-19 09:29:49 -0700485 }
mindypbc4142f2012-09-19 09:29:49 -0700486 });
487 }
488
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700489 protected void onConversationSeen() {
Scott Kennedy919d01a2013-05-07 16:13:29 -0700490 LogUtils.d(LOG_TAG, "AbstractConversationViewFragment#onConversationSeen()");
491
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700492 // Ignore unsafe calls made after a fragment is detached from an activity
493 final ControllableActivity activity = (ControllableActivity) getActivity();
494 if (activity == null) {
495 LogUtils.w(LOG_TAG, "ignoring onConversationSeen for conv=%s", mConversation.id);
496 return;
497 }
498
499 mViewState.setInfoForConversation(mConversation);
500
Scott Kennedy919d01a2013-05-07 16:13:29 -0700501 LogUtils.d(LOG_TAG, "onConversationSeen() - mSuppressMarkingViewed = %b",
502 mSuppressMarkingViewed);
Vikram Aggarwald82a31f2013-02-05 15:03:00 -0800503 // In most circumstances we want to mark the conversation as viewed and read, since the
504 // user has read it. However, if the user has already marked the conversation unread, we
505 // do not want a later mark-read operation to undo this. So we check this variable which
506 // is set in #markUnread() which suppresses automatic mark-read.
507 if (!mSuppressMarkingViewed) {
508 // mark viewed/read if not previously marked viewed by this conversation view,
509 // or if unread messages still exist in the message list cursor
510 // we don't want to keep marking viewed on rotation or restore
511 // but we do want future re-renders to mark read (e.g. "New message from X" case)
Paul Westbrook21452292013-04-15 18:51:07 -0700512 final MessageCursor cursor = getMessageCursor();
Scott Kennedy919d01a2013-05-07 16:13:29 -0700513 LogUtils.d(LOG_TAG, "onConversationSeen() - mConversation.isViewed() = %b, "
514 + "cursor null = %b, cursor.isConversationRead() = %b",
515 mConversation.isViewed(), cursor == null,
516 cursor != null && cursor.isConversationRead());
Vikram Aggarwald82a31f2013-02-05 15:03:00 -0800517 if (!mConversation.isViewed() || (cursor != null && !cursor.isConversationRead())) {
518 // Mark the conversation viewed and read.
519 activity.getConversationUpdater()
520 .markConversationsRead(Arrays.asList(mConversation), true, true);
Yu Ping Hu7c909c72013-01-18 11:58:01 -0800521
Vikram Aggarwald82a31f2013-02-05 15:03:00 -0800522 // and update the Message objects in the cursor so the next time a cursor update
523 // happens with these messages marked read, we know to ignore it
Paul Westbrook21452292013-04-15 18:51:07 -0700524 if (cursor != null && !cursor.isClosed()) {
Vikram Aggarwald82a31f2013-02-05 15:03:00 -0800525 cursor.markMessagesRead();
526 }
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700527 }
528 }
Scott Kennedy3b965d72013-06-25 14:36:55 -0700529 activity.getListHandler().onConversationSeen();
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700530 }
531
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700532 protected ConversationViewState getNewViewState() {
533 return new ConversationViewState();
534 }
535
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700536 private static class MessageLoader extends ObjectCursorLoader<ConversationMessage> {
mindypf4fce122012-09-14 15:55:33 -0700537 private boolean mDeliveredFirstResults = false;
mindypf4fce122012-09-14 15:55:33 -0700538
Andy Huang02133aa2012-11-08 19:50:57 -0800539 public MessageLoader(Context c, Uri messageListUri) {
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700540 super(c, messageListUri, UIProvider.MESSAGE_PROJECTION, ConversationMessage.FACTORY);
mindypf4fce122012-09-14 15:55:33 -0700541 }
542
543 @Override
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700544 public void deliverResult(ObjectCursor<ConversationMessage> result) {
mindypf4fce122012-09-14 15:55:33 -0700545 // We want to deliver these results, and then we want to make sure
546 // that any subsequent
547 // queries do not hit the network
548 super.deliverResult(result);
549
550 if (!mDeliveredFirstResults) {
551 mDeliveredFirstResults = true;
552 Uri uri = getUri();
553
554 // Create a ListParams that tells the provider to not hit the
555 // network
556 final ListParams listParams = new ListParams(ListParams.NO_LIMIT,
557 false /* useNetwork */);
558
559 // Build the new uri with this additional parameter
560 uri = uri
561 .buildUpon()
562 .appendQueryParameter(UIProvider.LIST_PARAMS_QUERY_PARAMETER,
563 listParams.serialize()).build();
564 setUri(uri);
565 }
566 }
Paul Westbrookc42ad5e2013-05-09 16:52:15 -0700567
568 @Override
569 protected ObjectCursor<ConversationMessage> getObjectCursor(Cursor inner) {
570 return new MessageCursor(inner);
571 }
mindypf4fce122012-09-14 15:55:33 -0700572 }
573
mindyp26d4d2d2012-09-18 17:30:32 -0700574 public abstract void onConversationUpdated(Conversation conversation);
575
Scott Kennedy18176782013-02-20 18:30:21 -0800576 public void onDetachedModeEntered() {
577 // If we have no messages, then we have nothing to display, so leave this view.
578 // Otherwise, just set the detached flag.
579 final Cursor messageCursor = getMessageCursor();
580
581 if (messageCursor == null || messageCursor.getCount() == 0) {
582 popOut();
583 } else {
584 mIsDetached = true;
585 }
586 }
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700587
588 /**
589 * Called when the JavaScript reports that it transformed a message.
590 * Sets a flag to true and invalidates the options menu so it will
591 * include the "Revert auto-sizing" menu option.
592 */
593 public void onConversationTransformed() {
594 mHasConversationBeenTransformed = true;
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700595 mHandler.post(new FragmentRunnable("invalidateOptionsMenu", this) {
Andrew Sappersteinae92e152013-05-03 13:55:18 -0700596 @Override
597 public void go() {
598 mActivity.invalidateOptionsMenu();
599 }
600 });
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700601 }
602
603 /**
604 * Called when the "Revert auto-sizing" option is selected. Default
605 * implementation simply sets a value on whether transforms should be
606 * applied. Derived classes should override this class and force a
607 * re-render so that the conversation renders without
608 */
609 public void showUntransformedConversation() {
610 // must set the value to true so we don't show the options menu item again
611 mHasConversationTransformBeenReverted = true;
612 }
613
614 /**
615 * Returns {@code true} if the conversation should be transformed. {@code false}, otherwise.
616 * @return {@code true} if the conversation should be transformed. {@code false}, otherwise.
617 */
618 public boolean shouldApplyTransforms() {
Alice Yang3617b412013-05-10 00:30:07 -0700619 return (mAccount.enableMessageTransforms > 0) &&
620 !mHasConversationTransformBeenReverted;
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700621 }
mindypf4fce122012-09-14 15:55:33 -0700622}