blob: 74a3d9ad7e55cede4722894e0fdf591295dfc33e [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
mindypff282d02012-09-17 10:33:02 -070020import android.animation.Animator;
Andy Huang9d3fd922012-09-26 22:23:58 -070021import android.animation.AnimatorInflater;
Andy Huang7d4746e2012-10-17 17:03:17 -070022import android.animation.AnimatorListenerAdapter;
mindypf4fce122012-09-14 15:55:33 -070023import android.app.Activity;
24import android.app.Fragment;
25import android.app.LoaderManager;
Paul Westbrook542fec92012-09-18 14:47:51 -070026import android.content.ActivityNotFoundException;
mindypf4fce122012-09-14 15:55:33 -070027import android.content.Context;
28import android.content.CursorLoader;
Paul Westbrook542fec92012-09-18 14:47:51 -070029import android.content.Intent;
mindypf4fce122012-09-14 15:55:33 -070030import android.content.Loader;
Paul Westbrook542fec92012-09-18 14:47:51 -070031import android.content.pm.ActivityInfo;
32import android.content.pm.PackageManager;
33import android.content.pm.ResolveInfo;
mindypff282d02012-09-17 10:33:02 -070034import android.content.res.Resources;
mindypf4fce122012-09-14 15:55:33 -070035import android.database.Cursor;
36import android.database.DataSetObservable;
37import android.database.DataSetObserver;
38import android.net.Uri;
39import android.os.Bundle;
mindypff282d02012-09-17 10:33:02 -070040import android.os.Handler;
Paul Westbrook542fec92012-09-18 14:47:51 -070041import android.provider.Browser;
mindypf4fce122012-09-14 15:55:33 -070042import android.view.Menu;
43import android.view.MenuInflater;
44import android.view.MenuItem;
mindypff282d02012-09-17 10:33:02 -070045import android.view.View;
Paul Westbrook542fec92012-09-18 14:47:51 -070046import android.webkit.WebView;
47import android.webkit.WebViewClient;
mindypf4fce122012-09-14 15:55:33 -070048
49import com.android.mail.ContactInfo;
50import com.android.mail.ContactInfoSource;
51import com.android.mail.FormattedDateBuilder;
52import com.android.mail.R;
53import com.android.mail.SenderInfoLoader;
Andy Huang8f187782012-11-06 17:49:25 -080054import com.android.mail.browse.ConversationAccountController;
mindypf4fce122012-09-14 15:55:33 -070055import com.android.mail.browse.ConversationViewHeader.ConversationViewHeaderCallbacks;
Andy Huang9d3fd922012-09-26 22:23:58 -070056import com.android.mail.browse.MessageCursor;
mindypf4fce122012-09-14 15:55:33 -070057import com.android.mail.browse.MessageCursor.ConversationController;
58import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks;
59import com.android.mail.providers.Account;
60import com.android.mail.providers.AccountObserver;
61import com.android.mail.providers.Address;
62import com.android.mail.providers.Conversation;
63import com.android.mail.providers.Folder;
64import com.android.mail.providers.ListParams;
65import com.android.mail.providers.UIProvider;
Vikram Aggarwala91d00b2013-01-18 12:00:37 -080066import com.android.mail.providers.UIProvider.CursorStatus;
mindypf4fce122012-09-14 15:55:33 -070067import com.android.mail.utils.LogTag;
68import com.android.mail.utils.LogUtils;
69import com.android.mail.utils.Utils;
70import com.google.common.collect.ImmutableMap;
71import com.google.common.collect.Maps;
72
Paul Westbrook4d8cad52012-09-21 14:13:49 -070073import java.util.Arrays;
Paul Westbrook542fec92012-09-18 14:47:51 -070074import java.util.List;
mindypf4fce122012-09-14 15:55:33 -070075import java.util.Map;
76import java.util.Set;
77
78public abstract class AbstractConversationViewFragment extends Fragment implements
79 ConversationController, ConversationAccountController, MessageHeaderViewCallbacks,
80 ConversationViewHeaderCallbacks {
81
82 private static final String ARG_ACCOUNT = "account";
83 public static final String ARG_CONVERSATION = "conversation";
84 private static final String ARG_FOLDER = "folder";
85 private static final String LOG_TAG = LogTag.getLogTag();
86 protected static final int MESSAGE_LOADER = 0;
87 protected static final int CONTACT_LOADER = 1;
mindyp32d911f2012-09-24 15:14:22 -070088 private static int sMinDelay = -1;
89 private static int sMinShowTime = -1;
mindypf4fce122012-09-14 15:55:33 -070090 protected ControllableActivity mActivity;
91 private final MessageLoaderCallbacks mMessageLoaderCallbacks = new MessageLoaderCallbacks();
92 protected FormattedDateBuilder mDateBuilder;
93 private final ContactLoaderCallbacks mContactLoaderCallbacks = new ContactLoaderCallbacks();
94 private MenuItem mChangeFoldersMenuItem;
95 protected Conversation mConversation;
96 protected Folder mFolder;
97 protected String mBaseUri;
98 protected Account mAccount;
99 protected final Map<String, Address> mAddressCache = Maps.newHashMap();
100 protected boolean mEnableContentReadySignal;
101 private MessageCursor mCursor;
102 private Context mContext;
Andy Huang9d3fd922012-09-26 22:23:58 -0700103 /**
104 * A backwards-compatible version of {{@link #getUserVisibleHint()}. Like the framework flag,
105 * this flag is saved and restored.
106 */
107 private boolean mUserVisible;
mindypff282d02012-09-17 10:33:02 -0700108 private View mProgressView;
109 private View mBackgroundView;
mindypff282d02012-09-17 10:33:02 -0700110 private final Handler mHandler = new Handler();
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700111
112 /**
113 * Parcelable state of the conversation view. Can safely be used without null checking any time
Andy Huang9d3fd922012-09-26 22:23:58 -0700114 * after {@link #onCreate(Bundle)}.
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700115 */
116 protected ConversationViewState mViewState;
117
mindyp32d911f2012-09-24 15:14:22 -0700118 private long mLoadingShownTime = -1;
119
Andy Huang9a8bc1e2012-10-23 19:48:25 -0700120 private final Runnable mDelayedShow = new FragmentRunnable("mDelayedShow") {
mindypff282d02012-09-17 10:33:02 -0700121 @Override
Andy Huang9a8bc1e2012-10-23 19:48:25 -0700122 public void go() {
mindyp32d911f2012-09-24 15:14:22 -0700123 mLoadingShownTime = System.currentTimeMillis();
Andy Huang2f2ad3f2012-10-16 15:31:32 -0700124 mProgressView.setVisibility(View.VISIBLE);
mindypff282d02012-09-17 10:33:02 -0700125 }
126 };
127
mindypf4fce122012-09-14 15:55:33 -0700128 private final AccountObserver mAccountObserver = new AccountObserver() {
129 @Override
130 public void onChanged(Account newAccount) {
Andy Huangadbf3e82012-10-13 13:30:19 -0700131 final Account oldAccount = mAccount;
mindypf4fce122012-09-14 15:55:33 -0700132 mAccount = newAccount;
Andy Huangadbf3e82012-10-13 13:30:19 -0700133 onAccountChanged(newAccount, oldAccount);
mindypf4fce122012-09-14 15:55:33 -0700134 }
135 };
136
Andy Huang9d3fd922012-09-26 22:23:58 -0700137 private static final String BUNDLE_VIEW_STATE =
138 AbstractConversationViewFragment.class.getName() + "viewstate";
Andy Huang9a8bc1e2012-10-23 19:48:25 -0700139 /**
140 * We save the user visible flag so the various transitions that occur during rotation do not
141 * cause unnecessary visibility change.
142 */
Andy Huang9d3fd922012-09-26 22:23:58 -0700143 private static final String BUNDLE_USER_VISIBLE =
144 AbstractConversationViewFragment.class.getName() + "uservisible";
145
mindypf4fce122012-09-14 15:55:33 -0700146 public static Bundle makeBasicArgs(Account account, Folder folder) {
147 Bundle args = new Bundle();
148 args.putParcelable(ARG_ACCOUNT, account);
149 args.putParcelable(ARG_FOLDER, folder);
150 return args;
151 }
152
153 /**
154 * Constructor needs to be public to handle orientation changes and activity
155 * lifecycle events.
156 */
157 public AbstractConversationViewFragment() {
158 super();
159 }
160
161 /**
162 * Subclasses must override, since this depends on how many messages are
163 * shown in the conversation view.
164 */
165 protected abstract void markUnread();
166
167 /**
168 * Subclasses must override this, since they may want to display a single or
169 * many messages related to this conversation.
170 */
Andy Huang014ea4c2012-09-25 14:50:54 -0700171 protected abstract void onMessageCursorLoadFinished(Loader<Cursor> loader,
172 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
192 final Bundle args = getArguments();
193 mAccount = args.getParcelable(ARG_ACCOUNT);
194 mConversation = args.getParcelable(ARG_CONVERSATION);
195 mFolder = args.getParcelable(ARG_FOLDER);
Paul Westbrookba4cce62012-09-28 10:24:20 -0700196
197 // Since the uri specified in the conversation base uri may not be unique, we specify a
198 // base uri that us guaranteed to be unique for this conversation.
199 mBaseUri = "x-thread://" + mAccount.name + "/" + mConversation.id;
200
mindypf4fce122012-09-14 15:55:33 -0700201 // On JB or newer, we use the 'webkitAnimationStart' DOM event to signal load complete
202 // Below JB, try to speed up initial render by having the webview do supplemental draws to
203 // custom a software canvas.
204 // TODO(mindyp):
205 //PAGE READINESS SIGNAL FOR JELLYBEAN AND NEWER
206 // Notify the app on 'webkitAnimationStart' of a simple dummy element with a simple no-op
207 // animation that immediately runs on page load. The app uses this as a signal that the
208 // content is loaded and ready to draw, since WebView delays firing this event until the
209 // layers are composited and everything is ready to draw.
210 // This signal does not seem to be reliable, so just use the old method for now.
211 mEnableContentReadySignal = false; //Utils.isRunningJellybeanOrLater();
212 LogUtils.d(LOG_TAG, "onCreate in ConversationViewFragment (this=%s)", this);
213 // Not really, we just want to get a crack to store a reference to the change_folder item
214 setHasOptionsMenu(true);
Andy Huang9d3fd922012-09-26 22:23:58 -0700215
216 if (savedState != null) {
217 mViewState = savedState.getParcelable(BUNDLE_VIEW_STATE);
218 mUserVisible = savedState.getBoolean(BUNDLE_USER_VISIBLE);
219 } else {
220 mViewState = getNewViewState();
221 }
mindypf4fce122012-09-14 15:55:33 -0700222 }
223
Scott Kennedyfafcd172012-11-01 17:23:33 -0700224 protected abstract WebView getWebView();
225
mindypff282d02012-09-17 10:33:02 -0700226 public void instantiateProgressIndicators(View rootView) {
mindypff282d02012-09-17 10:33:02 -0700227 mBackgroundView = rootView.findViewById(R.id.background_view);
mindypff282d02012-09-17 10:33:02 -0700228 mProgressView = rootView.findViewById(R.id.loading_progress);
229 }
230
231 protected void dismissLoadingStatus() {
Andy Huang7d4746e2012-10-17 17:03:17 -0700232 dismissLoadingStatus(null);
233 }
234
235 /**
236 * Begin the fade-out animation to hide the Progress overlay, either immediately or after some
237 * timeout (to ensure that the progress minimum time elapses).
238 *
239 * @param doAfter an optional Runnable action to execute after the animation completes
240 */
241 protected void dismissLoadingStatus(final Runnable doAfter) {
mindyp32d911f2012-09-24 15:14:22 -0700242 if (mLoadingShownTime == -1) {
mindypff282d02012-09-17 10:33:02 -0700243 // The runnable hasn't run yet, so just remove it.
244 mHandler.removeCallbacks(mDelayedShow);
Andy Huang7d4746e2012-10-17 17:03:17 -0700245 dismiss(doAfter);
mindypff282d02012-09-17 10:33:02 -0700246 return;
247 }
mindyp32d911f2012-09-24 15:14:22 -0700248 final long diff = Math.abs(System.currentTimeMillis() - mLoadingShownTime);
249 if (diff > sMinShowTime) {
Andy Huang7d4746e2012-10-17 17:03:17 -0700250 dismiss(doAfter);
mindyp32d911f2012-09-24 15:14:22 -0700251 } else {
Andy Huang9a8bc1e2012-10-23 19:48:25 -0700252 mHandler.postDelayed(new FragmentRunnable("dismissLoadingStatus") {
Andy Huang7d4746e2012-10-17 17:03:17 -0700253 @Override
Andy Huang9a8bc1e2012-10-23 19:48:25 -0700254 public void go() {
Andy Huang7d4746e2012-10-17 17:03:17 -0700255 dismiss(doAfter);
256 }
257 }, Math.abs(sMinShowTime - diff));
mindyp32d911f2012-09-24 15:14:22 -0700258 }
259 }
260
Andy Huang7d4746e2012-10-17 17:03:17 -0700261 private void dismiss(final Runnable doAfter) {
mindyp32d911f2012-09-24 15:14:22 -0700262 // Reset loading shown time.
263 mLoadingShownTime = -1;
Andy Huang9a8bc1e2012-10-23 19:48:25 -0700264 mProgressView.setVisibility(View.GONE);
mindypff282d02012-09-17 10:33:02 -0700265 if (mBackgroundView.getVisibility() == View.VISIBLE) {
Andy Huang9a8bc1e2012-10-23 19:48:25 -0700266 animateDismiss(doAfter);
mindypff282d02012-09-17 10:33:02 -0700267 } else {
Andy Huang30bcfe72012-10-18 18:09:03 -0700268 if (doAfter != null) {
269 doAfter.run();
270 }
mindypff282d02012-09-17 10:33:02 -0700271 }
272 }
273
Andy Huang9a8bc1e2012-10-23 19:48:25 -0700274 private void animateDismiss(final Runnable doAfter) {
275 // the animation can only work (and is only worth doing) if this fragment is added
276 // reasons it may not be added: fragment is being destroyed, or in the process of being
277 // restored
278 if (!isAdded()) {
279 mBackgroundView.setVisibility(View.GONE);
280 return;
281 }
282
Andy Huangbb6039e2012-10-24 18:42:28 -0700283 Utils.enableHardwareLayer(mBackgroundView);
Andy Huang9a8bc1e2012-10-23 19:48:25 -0700284 final Animator animator = AnimatorInflater.loadAnimator(getContext(), R.anim.fade_out);
285 animator.setTarget(mBackgroundView);
286 animator.addListener(new AnimatorListenerAdapter() {
287 @Override
288 public void onAnimationEnd(Animator animation) {
289 mBackgroundView.setVisibility(View.GONE);
290 mBackgroundView.setLayerType(View.LAYER_TYPE_NONE, null);
291 if (doAfter != null) {
292 doAfter.run();
293 }
294 }
295 });
296 animator.start();
297 }
298
mindypf4fce122012-09-14 15:55:33 -0700299 @Override
300 public void onActivityCreated(Bundle savedInstanceState) {
301 super.onActivityCreated(savedInstanceState);
Vikram Aggarwal8fe8ed42012-09-18 11:40:08 -0700302 final Activity activity = getActivity();
mindypf4fce122012-09-14 15:55:33 -0700303 if (!(activity instanceof ControllableActivity)) {
304 LogUtils.wtf(LOG_TAG, "ConversationViewFragment expects only a ControllableActivity to"
305 + "create it. Cannot proceed.");
306 }
Vikram Aggarwal8fe8ed42012-09-18 11:40:08 -0700307 if (activity == null || activity.isFinishing()) {
mindypf4fce122012-09-14 15:55:33 -0700308 // Activity is finishing, just bail.
309 return;
310 }
Vikram Aggarwal8fe8ed42012-09-18 11:40:08 -0700311 mActivity = (ControllableActivity) activity;
mindypf4fce122012-09-14 15:55:33 -0700312 mContext = activity.getApplicationContext();
313 mDateBuilder = new FormattedDateBuilder((Context) mActivity);
314 mAccount = mAccountObserver.initialize(mActivity.getAccountController());
315 }
316
317 @Override
318 public ConversationUpdater getListController() {
319 final ControllableActivity activity = (ControllableActivity) getActivity();
320 return activity != null ? activity.getConversationUpdater() : null;
321 }
322
mindypff282d02012-09-17 10:33:02 -0700323
324 protected void showLoadingStatus() {
mindyp32d911f2012-09-24 15:14:22 -0700325 if (!mUserVisible) {
326 return;
mindypff282d02012-09-17 10:33:02 -0700327 }
mindyp32d911f2012-09-24 15:14:22 -0700328 if (sMinDelay == -1) {
329 Resources res = getContext().getResources();
330 sMinDelay = res.getInteger(R.integer.conversationview_show_loading_delay);
331 sMinShowTime = res.getInteger(R.integer.conversationview_min_show_loading);
332 }
333 // If the loading view isn't already showing, show it and remove any
334 // pending calls to show the loading screen.
335 mBackgroundView.setVisibility(View.VISIBLE);
mindypff282d02012-09-17 10:33:02 -0700336 mHandler.removeCallbacks(mDelayedShow);
337 mHandler.postDelayed(mDelayedShow, sMinDelay);
338 }
339
mindypf4fce122012-09-14 15:55:33 -0700340 public Context getContext() {
341 return mContext;
342 }
343
Andy Huang02133aa2012-11-08 19:50:57 -0800344 @Override
mindypf4fce122012-09-14 15:55:33 -0700345 public Conversation getConversation() {
346 return mConversation;
347 }
348
349 @Override
350 public MessageCursor getMessageCursor() {
351 return mCursor;
352 }
353
mindypff282d02012-09-17 10:33:02 -0700354 public Handler getHandler() {
355 return mHandler;
356 }
357
mindypf4fce122012-09-14 15:55:33 -0700358 public MessageLoaderCallbacks getMessageLoaderCallbacks() {
359 return mMessageLoaderCallbacks;
360 }
361
362 public ContactLoaderCallbacks getContactInfoSource() {
363 return mContactLoaderCallbacks;
364 }
365
366 @Override
367 public Account getAccount() {
368 return mAccount;
369 }
370
371 @Override
372 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
373 super.onCreateOptionsMenu(menu, inflater);
374 mChangeFoldersMenuItem = menu.findItem(R.id.change_folder);
375 }
376
377 @Override
mindypf4fce122012-09-14 15:55:33 -0700378 public boolean onOptionsItemSelected(MenuItem item) {
379 boolean handled = false;
380 switch (item.getItemId()) {
381 case R.id.inside_conversation_unread:
382 markUnread();
383 handled = true;
384 break;
385 }
386 return handled;
387 }
388
389 // BEGIN conversation header callbacks
390 @Override
391 public void onFoldersClicked() {
392 if (mChangeFoldersMenuItem == null) {
393 LogUtils.e(LOG_TAG, "unable to open 'change folders' dialog for a conversation");
394 return;
395 }
396 mActivity.onOptionsItemSelected(mChangeFoldersMenuItem);
397 }
398
399 @Override
400 public String getSubjectRemainder(String subject) {
401 final SubjectDisplayChanger sdc = mActivity.getSubjectDisplayChanger();
402 if (sdc == null) {
403 return subject;
404 }
405 return sdc.getUnshownSubject(subject);
406 }
407 // END conversation header callbacks
408
409 @Override
Andy Huang9d3fd922012-09-26 22:23:58 -0700410 public void onSaveInstanceState(Bundle outState) {
411 if (mViewState != null) {
412 outState.putParcelable(BUNDLE_VIEW_STATE, mViewState);
413 }
414 outState.putBoolean(BUNDLE_USER_VISIBLE, mUserVisible);
415 }
416
417 @Override
mindypf4fce122012-09-14 15:55:33 -0700418 public void onDestroyView() {
419 super.onDestroyView();
420 mAccountObserver.unregisterAndDestroy();
421 }
422
423 /**
424 * {@link #setUserVisibleHint(boolean)} only works on API >= 15, so implement our own for
425 * reliability on older platforms.
426 */
427 public void setExtraUserVisibleHint(boolean isVisibleToUser) {
428 LogUtils.v(LOG_TAG, "in CVF.setHint, val=%s (%s)", isVisibleToUser, this);
429 if (mUserVisible != isVisibleToUser) {
430 mUserVisible = isVisibleToUser;
mindypbc4142f2012-09-19 09:29:49 -0700431 MessageCursor cursor = getMessageCursor();
mindyp0b9b48c2012-09-19 10:00:51 -0700432 if (mUserVisible && (cursor != null && cursor.isLoaded() && cursor.getCount() == 0)) {
mindypbc4142f2012-09-19 09:29:49 -0700433 // Pop back to conversation list and show error.
434 onError();
435 return;
436 }
mindypf4fce122012-09-14 15:55:33 -0700437 onUserVisibleHintChanged();
438 }
439 }
440
Andy Huang9d3fd922012-09-26 22:23:58 -0700441 public boolean isUserVisible() {
442 return mUserVisible;
443 }
444
mindypf4fce122012-09-14 15:55:33 -0700445 private class MessageLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
446
447 @Override
448 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
Andy Huang02133aa2012-11-08 19:50:57 -0800449 return new MessageLoader(mActivity.getActivityContext(), mConversation.messageListUri);
mindypf4fce122012-09-14 15:55:33 -0700450 }
451
452 @Override
453 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
454 // ignore truly duplicate results
455 // this can happen when restoring after rotation
456 if (mCursor == data) {
457 return;
458 } else {
Andy Huang6766b6e2012-09-28 12:43:52 -0700459 final MessageCursor messageCursor = (MessageCursor) data;
mindypf4fce122012-09-14 15:55:33 -0700460
Andy Huang02133aa2012-11-08 19:50:57 -0800461 // bind the cursor to this fragment so it can access to the current list controller
462 messageCursor.setController(AbstractConversationViewFragment.this);
463
mindypf4fce122012-09-14 15:55:33 -0700464 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
465 LogUtils.d(LOG_TAG, "LOADED CONVERSATION= %s", messageCursor.getDebugDump());
466 }
467
Vikram Aggarwala91d00b2013-01-18 12:00:37 -0800468 // We have no messages: exit conversation view.
469 if (messageCursor.getCount() == 0
470 && !CursorStatus.isWaitingForResults(messageCursor.getStatus())) {
mindypf4fce122012-09-14 15:55:33 -0700471 if (mUserVisible) {
mindypbc4142f2012-09-19 09:29:49 -0700472 onError();
mindypf4fce122012-09-14 15:55:33 -0700473 } else {
474 // we expect that the pager adapter will remove this
475 // conversation fragment on its own due to a separate
476 // conversation cursor update (we might get here if the
477 // message list update fires first. nothing to do
478 // because we expect to be torn down soon.)
479 LogUtils.i(LOG_TAG, "CVF: offscreen conv has no messages, ignoring update"
480 + " in anticipation of conv cursor update. c=%s", mConversation.uri);
481 }
Andy Huang233d4352012-10-18 14:00:24 -0700482 // existing mCursor will imminently be closed, must stop referencing it
483 // since we expect to be kicked out soon, it doesn't matter what mCursor
484 // becomes
485 mCursor = null;
mindypf4fce122012-09-14 15:55:33 -0700486 return;
487 }
488
489 // ignore cursors that are still loading results
490 if (!messageCursor.isLoaded()) {
Andy Huang233d4352012-10-18 14:00:24 -0700491 // existing mCursor will imminently be closed, must stop referencing it
492 // in this case, the new cursor is also no good, and since don't expect to get
493 // here except in initial load situations, it's safest to just ensure the
494 // reference is null
495 mCursor = null;
mindypf4fce122012-09-14 15:55:33 -0700496 return;
497 }
Andy Huang014ea4c2012-09-25 14:50:54 -0700498 final MessageCursor oldCursor = mCursor;
Andy Huang6766b6e2012-09-28 12:43:52 -0700499 mCursor = messageCursor;
Andy Huang014ea4c2012-09-25 14:50:54 -0700500 onMessageCursorLoadFinished(loader, mCursor, oldCursor);
mindypf4fce122012-09-14 15:55:33 -0700501 }
502 }
503
504 @Override
505 public void onLoaderReset(Loader<Cursor> loader) {
506 mCursor = null;
507 }
508
509 }
510
mindypbc4142f2012-09-19 09:29:49 -0700511 private void onError() {
512 // need to exit this view- conversation may have been
513 // deleted, or for whatever reason is now invalid (e.g.
514 // discard single draft)
515 //
516 // N.B. this may involve a fragment transaction, which
517 // FragmentManager will refuse to execute directly
518 // within onLoadFinished. Make sure the controller knows.
519 LogUtils.i(LOG_TAG, "CVF: visible conv has no messages, exiting conv mode");
520 // TODO(mindyp): handle ERROR status by showing an error
521 // message to the user that there are no messages in
522 // this conversation
Andy Huang9a8bc1e2012-10-23 19:48:25 -0700523 mHandler.post(new FragmentRunnable("onError") {
mindypbc4142f2012-09-19 09:29:49 -0700524
525 @Override
Andy Huang9a8bc1e2012-10-23 19:48:25 -0700526 public void go() {
mindypbc4142f2012-09-19 09:29:49 -0700527 mActivity.getListHandler()
528 .onConversationSelected(null, true /* inLoaderCallbacks */);
529 }
530
531 });
532 }
533
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700534 protected void onConversationSeen() {
535 // Ignore unsafe calls made after a fragment is detached from an activity
536 final ControllableActivity activity = (ControllableActivity) getActivity();
537 if (activity == null) {
538 LogUtils.w(LOG_TAG, "ignoring onConversationSeen for conv=%s", mConversation.id);
539 return;
540 }
541
542 mViewState.setInfoForConversation(mConversation);
543
544 // mark viewed/read if not previously marked viewed by this conversation view,
545 // or if unread messages still exist in the message list cursor
546 // we don't want to keep marking viewed on rotation or restore
547 // but we do want future re-renders to mark read (e.g. "New message from X" case)
548 MessageCursor cursor = getMessageCursor();
549 if (!mConversation.isViewed() || (cursor != null && !cursor.isConversationRead())) {
Yu Ping Hu7c909c72013-01-18 11:58:01 -0800550 // Mark the conversation viewed and read.
551 activity.getConversationUpdater().markConversationsRead(Arrays.asList(mConversation),
552 true /* read */, true /* viewed */);
553
554 // and update the Message objects in the cursor so the next time a cursor update happens
555 // with these messages marked read, we know to ignore it
556 if (cursor != null) {
557 cursor.markMessagesRead();
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700558 }
559 }
560
561 activity.getListHandler().onConversationSeen(mConversation);
Scott Kennedyfafcd172012-11-01 17:23:33 -0700562
563 showAutoFitPrompt();
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700564 }
565
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700566 protected ConversationViewState getNewViewState() {
567 return new ConversationViewState();
568 }
569
mindypf4fce122012-09-14 15:55:33 -0700570 private static class MessageLoader extends CursorLoader {
571 private boolean mDeliveredFirstResults = false;
mindypf4fce122012-09-14 15:55:33 -0700572
Andy Huang02133aa2012-11-08 19:50:57 -0800573 public MessageLoader(Context c, Uri messageListUri) {
574 super(c, messageListUri, UIProvider.MESSAGE_PROJECTION, null, null, null);
mindypf4fce122012-09-14 15:55:33 -0700575 }
576
577 @Override
578 public Cursor loadInBackground() {
Andy Huang02133aa2012-11-08 19:50:57 -0800579 return new MessageCursor(super.loadInBackground());
mindypf4fce122012-09-14 15:55:33 -0700580 }
581
582 @Override
583 public void deliverResult(Cursor result) {
584 // We want to deliver these results, and then we want to make sure
585 // that any subsequent
586 // queries do not hit the network
587 super.deliverResult(result);
588
589 if (!mDeliveredFirstResults) {
590 mDeliveredFirstResults = true;
591 Uri uri = getUri();
592
593 // Create a ListParams that tells the provider to not hit the
594 // network
595 final ListParams listParams = new ListParams(ListParams.NO_LIMIT,
596 false /* useNetwork */);
597
598 // Build the new uri with this additional parameter
599 uri = uri
600 .buildUpon()
601 .appendQueryParameter(UIProvider.LIST_PARAMS_QUERY_PARAMETER,
602 listParams.serialize()).build();
603 setUri(uri);
604 }
605 }
606 }
607
608 /**
609 * Inner class to to asynchronously load contact data for all senders in the conversation,
610 * and notify observers when the data is ready.
611 *
612 */
613 protected class ContactLoaderCallbacks implements ContactInfoSource,
614 LoaderManager.LoaderCallbacks<ImmutableMap<String, ContactInfo>> {
615
616 private Set<String> mSenders;
617 private ImmutableMap<String, ContactInfo> mContactInfoMap;
618 private DataSetObservable mObservable = new DataSetObservable();
619
620 public void setSenders(Set<String> emailAddresses) {
621 mSenders = emailAddresses;
622 }
623
624 @Override
625 public Loader<ImmutableMap<String, ContactInfo>> onCreateLoader(int id, Bundle args) {
626 return new SenderInfoLoader(mActivity.getActivityContext(), mSenders);
627 }
628
629 @Override
630 public void onLoadFinished(Loader<ImmutableMap<String, ContactInfo>> loader,
631 ImmutableMap<String, ContactInfo> data) {
632 mContactInfoMap = data;
633 mObservable.notifyChanged();
634 }
635
636 @Override
637 public void onLoaderReset(Loader<ImmutableMap<String, ContactInfo>> loader) {
638 }
639
640 @Override
641 public ContactInfo getContactInfo(String email) {
642 if (mContactInfoMap == null) {
643 return null;
644 }
645 return mContactInfoMap.get(email);
646 }
647
648 @Override
649 public void registerObserver(DataSetObserver observer) {
650 mObservable.registerObserver(observer);
651 }
652
653 @Override
654 public void unregisterObserver(DataSetObserver observer) {
655 mObservable.unregisterObserver(observer);
656 }
mindypf4fce122012-09-14 15:55:33 -0700657 }
Paul Westbrook542fec92012-09-18 14:47:51 -0700658
659 protected class AbstractConversationWebViewClient extends WebViewClient {
660 @Override
661 public boolean shouldOverrideUrlLoading(WebView view, String url) {
662 final Activity activity = getActivity();
663 if (activity == null) {
664 return false;
665 }
666
667 boolean result = false;
668 final Intent intent;
669 Uri uri = Uri.parse(url);
670 if (!Utils.isEmpty(mAccount.viewIntentProxyUri)) {
671 intent = generateProxyIntent(uri);
672 } else {
673 intent = new Intent(Intent.ACTION_VIEW, uri);
674 intent.putExtra(Browser.EXTRA_APPLICATION_ID, activity.getPackageName());
675 }
676
677 try {
678 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
679 activity.startActivity(intent);
680 result = true;
681 } catch (ActivityNotFoundException ex) {
682 // If no application can handle the URL, assume that the
683 // caller can handle it.
684 }
685
686 return result;
687 }
688
689 private Intent generateProxyIntent(Uri uri) {
690 final Intent intent = new Intent(Intent.ACTION_VIEW, mAccount.viewIntentProxyUri);
691 intent.putExtra(UIProvider.ViewProxyExtras.EXTRA_ORIGINAL_URI, uri);
692 intent.putExtra(UIProvider.ViewProxyExtras.EXTRA_ACCOUNT, mAccount);
693
694 final Context context = getContext();
695 PackageManager manager = null;
696 // We need to catch the exception to make CanvasConversationHeaderView
697 // test pass. Bug: http://b/issue?id=3470653.
698 try {
699 manager = context.getPackageManager();
700 } catch (UnsupportedOperationException e) {
701 LogUtils.e(LOG_TAG, e, "Error getting package manager");
702 }
703
704 if (manager != null) {
705 // Try and resolve the intent, to find an activity from this package
706 final List<ResolveInfo> resolvedActivities = manager.queryIntentActivities(
707 intent, PackageManager.MATCH_DEFAULT_ONLY);
708
709 final String packageName = context.getPackageName();
710
711 // Now try and find one that came from this package, if one is not found, the UI
712 // provider must have specified an intent that is to be handled by a different apk.
713 // In that case, the class name will not be set on the intent, so the default
714 // intent resolution will be used.
715 for (ResolveInfo resolveInfo: resolvedActivities) {
716 final ActivityInfo activityInfo = resolveInfo.activityInfo;
717 if (packageName.equals(activityInfo.packageName)) {
718 intent.setClassName(activityInfo.packageName, activityInfo.name);
719 break;
720 }
721 }
722 }
723
724 return intent;
725 }
726 }
727
mindyp26d4d2d2012-09-18 17:30:32 -0700728 public abstract void onConversationUpdated(Conversation conversation);
729
Andy Huang9d3fd922012-09-26 22:23:58 -0700730 /**
731 * Small Runnable-like wrapper that first checks that the Fragment is in a good state before
732 * doing any work. Ideal for use with a {@link Handler}.
733 */
734 protected abstract class FragmentRunnable implements Runnable {
735
736 private final String mOpName;
737
738 public FragmentRunnable(String opName) {
739 mOpName = opName;
740 }
741
742 public abstract void go();
743
744 @Override
745 public void run() {
746 if (!isAdded()) {
747 LogUtils.i(LOG_TAG, "Unable to run op='%s' b/c fragment is not attached: %s",
748 mOpName, AbstractConversationViewFragment.this);
749 return;
750 }
751 go();
752 }
753
754 }
755
Scott Kennedyfafcd172012-11-01 17:23:33 -0700756 private static boolean isConversationViewModeSet(final Account acct) {
757 return acct.settings.conversationViewMode != UIProvider.ConversationViewMode.UNDEFINED;
758 }
759
760 private void showAutoFitPrompt() {
761 // If the user has never set a conversation view mode, and they view a wide message, we
762 // should prompt them to turn on auto-fit
763 final boolean enablePrompt =
764 getResources().getInteger(R.integer.prompt_auto_fit_on_first_wide_message) == 1;
765 // TODO: Enable this dialog for Email and ensure it saves the setting properly, and remove
766 // R.integer.prompt_auto_fit_on_first_wide_message
767 if (enablePrompt && isUserVisible() && !isConversationViewModeSet(mAccount)) {
768 final boolean isWideContent =
769 getWebView().canScrollHorizontally(1) || getWebView().canScrollHorizontally(-1);
770
771 final boolean dialogShowing =
772 getFragmentManager().findFragmentByTag(AutoFitPromptDialogFragment.FRAGMENT_TAG)
773 != null;
774
775 if (isWideContent && !dialogShowing) {
776 // Not everything fits, so let's prompt them to set an auto-fit value
777 AutoFitPromptDialogFragment.newInstance(mAccount.updateSettingsUri)
778 .show(getFragmentManager(), AutoFitPromptDialogFragment.FRAGMENT_TAG);
779 }
780 }
781 }
mindypf4fce122012-09-14 15:55:33 -0700782}