blob: f11152b1db5e2dfa8bb3c67a2b84f0f43b8cf18f [file] [log] [blame]
Mindy Pereira9b875682012-02-15 18:10:54 -08001/*
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
mindyp3bcf1802012-09-09 11:17:00 -070020import android.animation.Animator;
21import android.animation.AnimatorInflater;
22import android.animation.Animator.AnimatorListener;
Mindy Pereira9b875682012-02-15 18:10:54 -080023import android.app.Activity;
24import android.app.Fragment;
Mindy Pereira8e915722012-02-16 14:42:56 -080025import android.app.LoaderManager;
Andy Huangaf5d4e02012-03-19 19:02:12 -070026import android.content.ActivityNotFoundException;
Mindy Pereira9b875682012-02-15 18:10:54 -080027import android.content.Context;
Mindy Pereira465d03d2012-02-16 15:17:46 -080028import android.content.CursorLoader;
Andy Huangaf5d4e02012-03-19 19:02:12 -070029import android.content.Intent;
Mindy Pereira8e915722012-02-16 14:42:56 -080030import android.content.Loader;
mindyp3bcf1802012-09-09 11:17:00 -070031import android.content.res.Resources;
Mindy Pereira9b875682012-02-15 18:10:54 -080032import android.database.Cursor;
Andy Huangb8331b42012-07-16 19:08:53 -070033import android.database.DataSetObservable;
34import android.database.DataSetObserver;
Mindy Pereira465d03d2012-02-16 15:17:46 -080035import android.net.Uri;
Paul Westbrookcebcc642012-08-08 10:06:04 -070036import android.os.AsyncTask;
Mindy Pereira9b875682012-02-15 18:10:54 -080037import android.os.Bundle;
Andy Huangf70fc402012-02-17 15:37:42 -080038import android.os.Handler;
mindyp3bcf1802012-09-09 11:17:00 -070039import android.os.SystemClock;
Andy Huangaf5d4e02012-03-19 19:02:12 -070040import android.provider.Browser;
mindyp3bcf1802012-09-09 11:17:00 -070041import android.text.Spannable;
42import android.text.SpannableStringBuilder;
Andy Huang47aa9c92012-07-31 15:37:21 -070043import android.text.TextUtils;
mindyp3bcf1802012-09-09 11:17:00 -070044import android.text.style.ForegroundColorSpan;
Mindy Pereira9b875682012-02-15 18:10:54 -080045import android.view.LayoutInflater;
Andy Huang5ff63742012-03-16 20:30:23 -070046import android.view.Menu;
47import android.view.MenuInflater;
48import android.view.MenuItem;
Mindy Pereira9b875682012-02-15 18:10:54 -080049import android.view.View;
50import android.view.ViewGroup;
Andy Huangf70fc402012-02-17 15:37:42 -080051import android.webkit.ConsoleMessage;
Paul Westbrookcebcc642012-08-08 10:06:04 -070052import android.webkit.CookieManager;
53import android.webkit.CookieSyncManager;
Andy Huangf70fc402012-02-17 15:37:42 -080054import android.webkit.WebChromeClient;
55import android.webkit.WebSettings;
Andy Huang17a9cde2012-03-09 18:03:16 -080056import android.webkit.WebView;
57import android.webkit.WebViewClient;
Andy Huang47aa9c92012-07-31 15:37:21 -070058import android.widget.TextView;
Mindy Pereira9b875682012-02-15 18:10:54 -080059
Andy Huangb8331b42012-07-16 19:08:53 -070060import com.android.mail.ContactInfo;
61import com.android.mail.ContactInfoSource;
Andy Huang59e0b182012-08-14 14:32:23 -070062import com.android.mail.FormattedDateBuilder;
Mindy Pereira9b875682012-02-15 18:10:54 -080063import com.android.mail.R;
Andy Huangb8331b42012-07-16 19:08:53 -070064import com.android.mail.SenderInfoLoader;
Andy Huang5ff63742012-03-16 20:30:23 -070065import com.android.mail.browse.ConversationContainer;
Andy Huang46dfba62012-04-19 01:47:32 -070066import com.android.mail.browse.ConversationOverlayItem;
Andy Huang7bdc3752012-03-25 17:18:19 -070067import com.android.mail.browse.ConversationViewAdapter;
Andy Huang28b7aee2012-08-20 20:27:32 -070068import com.android.mail.browse.ConversationViewAdapter.ConversationAccountController;
Andy Huang46dfba62012-04-19 01:47:32 -070069import com.android.mail.browse.ConversationViewAdapter.MessageFooterItem;
Andy Huang7bdc3752012-03-25 17:18:19 -070070import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem;
Andy Huang46dfba62012-04-19 01:47:32 -070071import com.android.mail.browse.ConversationViewAdapter.SuperCollapsedBlockItem;
Andy Huang5ff63742012-03-16 20:30:23 -070072import com.android.mail.browse.ConversationViewHeader;
73import com.android.mail.browse.ConversationWebView;
Andy Huang7bdc3752012-03-25 17:18:19 -070074import com.android.mail.browse.MessageCursor;
Andy Huangcd5c5ee2012-08-12 19:03:51 -070075import com.android.mail.browse.MessageCursor.ConversationController;
Andy Huang28b7aee2012-08-20 20:27:32 -070076import com.android.mail.browse.MessageCursor.ConversationMessage;
Andy Huang59e0b182012-08-14 14:32:23 -070077import com.android.mail.browse.MessageHeaderView;
Andy Huang3233bff2012-03-20 19:38:45 -070078import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks;
Andy Huang46dfba62012-04-19 01:47:32 -070079import com.android.mail.browse.SuperCollapsedBlock;
Andy Huang0b7ed6f2012-07-25 19:23:26 -070080import com.android.mail.browse.WebViewContextMenu;
Mindy Pereira9b875682012-02-15 18:10:54 -080081import com.android.mail.providers.Account;
Vikram Aggarwal7c401b72012-08-13 16:43:47 -070082import com.android.mail.providers.AccountObserver;
Andy Huang65fe28f2012-04-06 18:08:53 -070083import com.android.mail.providers.Address;
Mindy Pereira9b875682012-02-15 18:10:54 -080084import com.android.mail.providers.Conversation;
Mindy Pereira863e4412012-03-23 12:21:38 -070085import com.android.mail.providers.Folder;
Paul Westbrook2d82d612012-03-07 09:21:14 -080086import com.android.mail.providers.ListParams;
Andy Huangf70fc402012-02-17 15:37:42 -080087import com.android.mail.providers.Message;
Mindy Pereira9b875682012-02-15 18:10:54 -080088import com.android.mail.providers.UIProvider;
Mindy Pereira863e4412012-03-23 12:21:38 -070089import com.android.mail.providers.UIProvider.AccountCapabilities;
90import com.android.mail.providers.UIProvider.FolderCapabilities;
Mark Wei9982fdb2012-08-30 18:27:46 -070091import com.android.mail.providers.UIProvider.ViewProxyExtras;
Andy Huangcd5c5ee2012-08-12 19:03:51 -070092import com.android.mail.ui.ConversationViewState.ExpansionState;
Paul Westbrookb334c902012-06-25 11:42:46 -070093import com.android.mail.utils.LogTag;
Mindy Pereira9b875682012-02-15 18:10:54 -080094import com.android.mail.utils.LogUtils;
Andy Huang2e9acfe2012-03-15 22:39:36 -070095import com.android.mail.utils.Utils;
Andy Huangb8331b42012-07-16 19:08:53 -070096import com.google.common.collect.ImmutableMap;
Andy Huang46dfba62012-04-19 01:47:32 -070097import com.google.common.collect.Lists;
Andy Huang65fe28f2012-04-06 18:08:53 -070098import com.google.common.collect.Maps;
Andy Huangb8331b42012-07-16 19:08:53 -070099import com.google.common.collect.Sets;
Andy Huang65fe28f2012-04-06 18:08:53 -0700100
Andy Huang839ada22012-07-20 15:48:40 -0700101import java.util.Arrays;
Andy Huang46dfba62012-04-19 01:47:32 -0700102import java.util.List;
Andy Huang65fe28f2012-04-06 18:08:53 -0700103import java.util.Map;
Andy Huangb8331b42012-07-16 19:08:53 -0700104import java.util.Set;
Mindy Pereira9b875682012-02-15 18:10:54 -0800105
Andy Huangf70fc402012-02-17 15:37:42 -0800106
Mindy Pereira9b875682012-02-15 18:10:54 -0800107/**
108 * The conversation view UI component.
109 */
Mindy Pereira8e915722012-02-16 14:42:56 -0800110public final class ConversationViewFragment extends Fragment implements
Andy Huang3233bff2012-03-20 19:38:45 -0700111 ConversationViewHeader.ConversationViewHeaderCallbacks,
Andy Huang46dfba62012-04-19 01:47:32 -0700112 MessageHeaderViewCallbacks,
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700113 SuperCollapsedBlock.OnClickListener,
Andy Huang28b7aee2012-08-20 20:27:32 -0700114 ConversationController,
115 ConversationAccountController {
Mindy Pereira8e915722012-02-16 14:42:56 -0800116
Paul Westbrookb334c902012-06-25 11:42:46 -0700117 private static final String LOG_TAG = LogTag.getLogTag();
Andy Huang632721e2012-04-11 16:57:26 -0700118 public static final String LAYOUT_TAG = "ConvLayout";
Mindy Pereira9b875682012-02-15 18:10:54 -0800119
Mindy Pereira8e915722012-02-16 14:42:56 -0800120 private static final int MESSAGE_LOADER_ID = 0;
Andy Huangb8331b42012-07-16 19:08:53 -0700121 private static final int CONTACT_LOADER_ID = 1;
Mindy Pereira9b875682012-02-15 18:10:54 -0800122
mindyp3bcf1802012-09-09 11:17:00 -0700123 /** Do not auto load data when create this {@link ConversationView}. */
124 public static final int NO_AUTO_LOAD = 0;
125 /** Auto load data but do not show any animation. */
126 public static final int AUTO_LOAD_BACKGROUND = 1;
127 /** Auto load data and show animation. */
128 public static final int AUTO_LOAD_VISIBLE = 2;
129
Mindy Pereira8e915722012-02-16 14:42:56 -0800130 private ControllableActivity mActivity;
Mindy Pereira9b875682012-02-15 18:10:54 -0800131
Andy Huangf70fc402012-02-17 15:37:42 -0800132 private Context mContext;
133
134 private Conversation mConversation;
Mindy Pereira9b875682012-02-15 18:10:54 -0800135
Andy Huangf70fc402012-02-17 15:37:42 -0800136 private ConversationContainer mConversationContainer;
Mindy Pereira9b875682012-02-15 18:10:54 -0800137
Andy Huangf70fc402012-02-17 15:37:42 -0800138 private Account mAccount;
Mindy Pereira9b875682012-02-15 18:10:54 -0800139
Andy Huangf70fc402012-02-17 15:37:42 -0800140 private ConversationWebView mWebView;
Mindy Pereira9b875682012-02-15 18:10:54 -0800141
Andy Huang47aa9c92012-07-31 15:37:21 -0700142 private View mNewMessageBar;
143
mindyp3bcf1802012-09-09 11:17:00 -0700144 private View mBackgroundView;
145
146 private View mInfoView;
147
148 private TextView mSendersView;
149
150 private TextView mSubjectView;
151
152 private View mProgressView;
153
Andy Huangf70fc402012-02-17 15:37:42 -0800154 private HtmlConversationTemplates mTemplates;
155
156 private String mBaseUri;
157
158 private final Handler mHandler = new Handler();
159
160 private final MailJsBridge mJsBridge = new MailJsBridge();
161
Andy Huang17a9cde2012-03-09 18:03:16 -0800162 private final WebViewClient mWebViewClient = new ConversationWebViewClient();
163
Andy Huang7bdc3752012-03-25 17:18:19 -0700164 private ConversationViewAdapter mAdapter;
165 private MessageCursor mCursor;
Andy Huang51067132012-03-12 20:08:19 -0700166
167 private boolean mViewsCreated;
168
Andy Huang5ff63742012-03-16 20:30:23 -0700169 private MenuItem mChangeFoldersMenuItem;
Andy Huang632721e2012-04-11 16:57:26 -0700170 /**
171 * Folder is used to help determine valid menu actions for this conversation.
172 */
Mindy Pereira863e4412012-03-23 12:21:38 -0700173 private Folder mFolder;
174
Andy Huang65fe28f2012-04-06 18:08:53 -0700175 private final Map<String, Address> mAddressCache = Maps.newHashMap();
176
Andy Huang46dfba62012-04-19 01:47:32 -0700177 /**
178 * Temporary string containing the message bodies of the messages within a super-collapsed
179 * block, for one-time use during block expansion. We cannot easily pass the body HTML
180 * into JS without problematic escaping, so hold onto it momentarily and signal JS to fetch it
181 * using {@link MailJsBridge}.
182 */
183 private String mTempBodiesHtml;
184
Andy Huang632721e2012-04-11 16:57:26 -0700185 private boolean mUserVisible;
186
187 private int mMaxAutoLoadMessages;
188
189 private boolean mDeferredConversationLoad;
190
Andy Huang839ada22012-07-20 15:48:40 -0700191 /**
Andy Huangc11011d2012-07-24 18:50:34 -0700192 * Handles a deferred 'mark read' operation, necessary when the conversation view has finished
193 * loading before the conversation cursor. Normally null unless this situation occurs.
194 * When finally able to 'mark read', this observer will also be unregistered and cleaned up.
195 */
196 private MarkReadObserver mMarkReadObserver;
197
198 /**
Andy Huang839ada22012-07-20 15:48:40 -0700199 * Parcelable state of the conversation view. Can safely be used without null checking any time
200 * after {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}.
201 */
202 private ConversationViewState mViewState;
203
Andy Huangb8331b42012-07-16 19:08:53 -0700204 private final MessageLoaderCallbacks mMessageLoaderCallbacks = new MessageLoaderCallbacks();
205 private final ContactLoaderCallbacks mContactLoaderCallbacks = new ContactLoaderCallbacks();
206
Andy Huang28b7aee2012-08-20 20:27:32 -0700207 private final AccountObserver mAccountObserver = new AccountObserver() {
208 @Override
209 public void onChanged(Account newAccount) {
210 mAccount = newAccount;
211
212 // settings may have been updated; refresh views that are known to depend on settings
213 mConversationContainer.getSnapHeader().onAccountChanged();
214 mAdapter.notifyDataSetChanged();
215 }
216 };
mindyp3bcf1802012-09-09 11:17:00 -0700217 private boolean mEnableContentReadySignal;
Andy Huang28b7aee2012-08-20 20:27:32 -0700218
Andy Huangf70fc402012-02-17 15:37:42 -0800219 private static final String ARG_ACCOUNT = "account";
Andy Huang632721e2012-04-11 16:57:26 -0700220 public static final String ARG_CONVERSATION = "conversation";
Mindy Pereira863e4412012-03-23 12:21:38 -0700221 private static final String ARG_FOLDER = "folder";
Andy Huang839ada22012-07-20 15:48:40 -0700222 private static final String BUNDLE_VIEW_STATE = "viewstate";
mindyp3bcf1802012-09-09 11:17:00 -0700223 private static int sSubjectColor = Integer.MIN_VALUE;
224 private static int sSnippetColor = Integer.MIN_VALUE;
Andy Huangf70fc402012-02-17 15:37:42 -0800225
Andy Huangbd544e32012-05-29 15:56:51 -0700226 private static final boolean DEBUG_DUMP_CONVERSATION_HTML = false;
Andy Huang47aa9c92012-07-31 15:37:21 -0700227 private static final boolean DISABLE_OFFSCREEN_LOADING = false;
mindyp3bcf1802012-09-09 11:17:00 -0700228 protected static final String AUTO_LOAD_KEY = "auto-load";
Andy Huangbd544e32012-05-29 15:56:51 -0700229
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800230 /**
231 * Constructor needs to be public to handle orientation changes and activity lifecycle events.
232 */
Andy Huangf70fc402012-02-17 15:37:42 -0800233 public ConversationViewFragment() {
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800234 super();
Mindy Pereira9b875682012-02-15 18:10:54 -0800235 }
236
237 /**
238 * Creates a new instance of {@link ConversationViewFragment}, initialized
Andy Huang632721e2012-04-11 16:57:26 -0700239 * to display a conversation with other parameters inherited/copied from an existing bundle,
240 * typically one created using {@link #makeBasicArgs}.
241 */
242 public static ConversationViewFragment newInstance(Bundle existingArgs,
243 Conversation conversation) {
244 ConversationViewFragment f = new ConversationViewFragment();
245 Bundle args = new Bundle(existingArgs);
246 args.putParcelable(ARG_CONVERSATION, conversation);
247 f.setArguments(args);
248 return f;
249 }
250
251 public static Bundle makeBasicArgs(Account account, Folder folder) {
252 Bundle args = new Bundle();
253 args.putParcelable(ARG_ACCOUNT, account);
254 args.putParcelable(ARG_FOLDER, folder);
255 return args;
256 }
257
Mindy Pereira9b875682012-02-15 18:10:54 -0800258 @Override
259 public void onActivityCreated(Bundle savedInstanceState) {
Andy Huang632721e2012-04-11 16:57:26 -0700260 LogUtils.d(LOG_TAG, "IN CVF.onActivityCreated, this=%s subj=%s", this,
261 mConversation.subject);
Mindy Pereira9b875682012-02-15 18:10:54 -0800262 super.onActivityCreated(savedInstanceState);
263 // Strictly speaking, we get back an android.app.Activity from getActivity. However, the
264 // only activity creating a ConversationListContext is a MailActivity which is of type
265 // ControllableActivity, so this cast should be safe. If this cast fails, some other
266 // activity is creating ConversationListFragments. This activity must be of type
267 // ControllableActivity.
268 final Activity activity = getActivity();
Mindy Pereira863e4412012-03-23 12:21:38 -0700269 if (!(activity instanceof ControllableActivity)) {
Andy Huangf70fc402012-02-17 15:37:42 -0800270 LogUtils.wtf(LOG_TAG, "ConversationViewFragment expects only a ControllableActivity to"
271 + "create it. Cannot proceed.");
Mindy Pereira9b875682012-02-15 18:10:54 -0800272 }
273 mActivity = (ControllableActivity) activity;
Andy Huangf70fc402012-02-17 15:37:42 -0800274 mContext = mActivity.getApplicationContext();
Mindy Pereira9b875682012-02-15 18:10:54 -0800275 if (mActivity.isFinishing()) {
276 // Activity is finishing, just bail.
277 return;
278 }
Andy Huangf70fc402012-02-17 15:37:42 -0800279 mTemplates = new HtmlConversationTemplates(mContext);
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700280 mAccount = mAccountObserver.initialize(mActivity.getAccountController());
Andy Huang59e0b182012-08-14 14:32:23 -0700281
282 final FormattedDateBuilder dateBuilder = new FormattedDateBuilder(mContext);
283
Andy Huang28b7aee2012-08-20 20:27:32 -0700284 mAdapter = new ConversationViewAdapter(mActivity.getActivityContext(), this,
Andy Huang59e0b182012-08-14 14:32:23 -0700285 getLoaderManager(), this, mContactLoaderCallbacks, this, this, mAddressCache,
286 dateBuilder);
Andy Huang51067132012-03-12 20:08:19 -0700287 mConversationContainer.setOverlayAdapter(mAdapter);
288
Andy Huang59e0b182012-08-14 14:32:23 -0700289 // set up snap header (the adapter usually does this with the other ones)
290 final MessageHeaderView snapHeader = mConversationContainer.getSnapHeader();
Andy Huang28b7aee2012-08-20 20:27:32 -0700291 snapHeader.initialize(dateBuilder, this, mAddressCache);
Andy Huang59e0b182012-08-14 14:32:23 -0700292 snapHeader.setCallbacks(this);
293 snapHeader.setContactInfoSource(mContactLoaderCallbacks);
294
Andy Huang632721e2012-04-11 16:57:26 -0700295 mMaxAutoLoadMessages = getResources().getInteger(R.integer.max_auto_load_messages);
296
Andy Huang0b7ed6f2012-07-25 19:23:26 -0700297 mWebView.setOnCreateContextMenuListener(new WebViewContextMenu(activity));
298
Mindy Pereira9b875682012-02-15 18:10:54 -0800299 showConversation();
Paul Westbrookcebcc642012-08-08 10:06:04 -0700300
301 if (mConversation.conversationBaseUri != null &&
302 !TextUtils.isEmpty(mConversation.conversationCookie)) {
303 // Set the cookie for this base url
304 new SetCookieTask(mConversation.conversationBaseUri.toString(),
305 mConversation.conversationCookie).execute();
306 }
Mindy Pereira9b875682012-02-15 18:10:54 -0800307 }
308
309 @Override
310 public void onCreate(Bundle savedState) {
Mindy Pereira9b875682012-02-15 18:10:54 -0800311 super.onCreate(savedState);
Andy Huangf70fc402012-02-17 15:37:42 -0800312
Vikram Aggarwalf76bf4c2012-08-01 11:21:05 -0700313 final Bundle args = getArguments();
Andy Huangf70fc402012-02-17 15:37:42 -0800314 mAccount = args.getParcelable(ARG_ACCOUNT);
315 mConversation = args.getParcelable(ARG_CONVERSATION);
Mindy Pereira863e4412012-03-23 12:21:38 -0700316 mFolder = args.getParcelable(ARG_FOLDER);
Paul Westbrookcebcc642012-08-08 10:06:04 -0700317 // Since the uri specified in the conversation base uri may not be unique, we specify a
318 // base uri that us guaranteed to be unique for this conversation.
319 mBaseUri = "x-thread://" + mAccount.name + "/" + mConversation.id;
Andy Huang1ee96b22012-08-24 20:19:53 -0700320 LogUtils.d(LOG_TAG, "onCreate in ConversationViewFragment (this=%s)", this);
321
Vikram Aggarwald503df42012-05-11 10:13:35 -0700322 // Not really, we just want to get a crack to store a reference to the change_folder item
Andy Huang5ff63742012-03-16 20:30:23 -0700323 setHasOptionsMenu(true);
Mindy Pereira9b875682012-02-15 18:10:54 -0800324 }
325
mindyp3bcf1802012-09-09 11:17:00 -0700326 private CharSequence createSubjectSnippet(CharSequence subject, CharSequence snippet) {
327 SpannableStringBuilder subjectText = new SpannableStringBuilder(mContext.getString(
328 R.string.subject_and_snippet, subject, snippet));
329 ensureSubjectSnippetColors();
330 int snippetStart = 0;
331 int fontColor = sSubjectColor;
332 if (subject != null) {
333 subjectText.setSpan(new ForegroundColorSpan(fontColor), 0, subject.length(),
334 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
335 snippetStart = subject.length() + 1;
336 }
337 if (snippet != null) {
338 fontColor = sSnippetColor;
339 subjectText.setSpan(new ForegroundColorSpan(fontColor), snippetStart, subjectText
340 .length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
341 }
342 return subjectText;
343 }
344
345 private void ensureSubjectSnippetColors() {
346 if (sSubjectColor == Integer.MIN_VALUE) {
347 Resources res = mContext.getResources();
348 sSubjectColor = res.getColor(R.color.subject_text_color_read);
349 sSnippetColor = res.getColor(R.color.snippet_text_color_read);
350 }
351 }
352
Mindy Pereira9b875682012-02-15 18:10:54 -0800353 @Override
354 public View onCreateView(LayoutInflater inflater,
355 ViewGroup container, Bundle savedInstanceState) {
Andy Huang839ada22012-07-20 15:48:40 -0700356
357 if (savedInstanceState != null) {
358 mViewState = savedInstanceState.getParcelable(BUNDLE_VIEW_STATE);
359 } else {
360 mViewState = new ConversationViewState();
361 }
362
Andy Huang632721e2012-04-11 16:57:26 -0700363 View rootView = inflater.inflate(R.layout.conversation_view, container, false);
Andy Huangf70fc402012-02-17 15:37:42 -0800364 mConversationContainer = (ConversationContainer) rootView
365 .findViewById(R.id.conversation_container);
Andy Huang47aa9c92012-07-31 15:37:21 -0700366
367 mNewMessageBar = mConversationContainer.findViewById(R.id.new_message_notification_bar);
368 mNewMessageBar.setOnClickListener(new View.OnClickListener() {
369 @Override
370 public void onClick(View v) {
371 onNewMessageBarClick();
372 }
373 });
374
mindyp3bcf1802012-09-09 11:17:00 -0700375 mBackgroundView = rootView.findViewById(R.id.background_view);
376 mInfoView = rootView.findViewById(R.id.info_view);
377 mSendersView = (TextView) rootView.findViewById(R.id.senders_view);
378 mSubjectView = (TextView) rootView.findViewById(R.id.info_subject_view);
379 mProgressView = rootView.findViewById(R.id.loading_progress);
380
Andy Huang5ff63742012-03-16 20:30:23 -0700381 mWebView = (ConversationWebView) mConversationContainer.findViewById(R.id.webview);
Andy Huangf70fc402012-02-17 15:37:42 -0800382
Andy Huangf70fc402012-02-17 15:37:42 -0800383 mWebView.addJavascriptInterface(mJsBridge, "mail");
mindyp3bcf1802012-09-09 11:17:00 -0700384 // On JB or newer, we use the 'webkitAnimationStart' DOM event to signal load complete
385 // Below JB, try to speed up initial render by having the webview do supplemental draws to
386 // custom a software canvas.
387 mEnableContentReadySignal = Utils.isRunningJellybeanOrLater();
Andy Huang17a9cde2012-03-09 18:03:16 -0800388 mWebView.setWebViewClient(mWebViewClient);
Andy Huangf70fc402012-02-17 15:37:42 -0800389 mWebView.setWebChromeClient(new WebChromeClient() {
390 @Override
391 public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
392 LogUtils.i(LOG_TAG, "JS: %s (%s:%d)", consoleMessage.message(),
393 consoleMessage.sourceId(), consoleMessage.lineNumber());
394 return true;
395 }
396 });
Andy Huang23014702012-07-09 12:50:36 -0700397 mWebView.setContentSizeChangeListener(new ConversationWebView.ContentSizeChangeListener() {
398 @Override
399 public void onHeightChange(int h) {
400 // When WebKit says the DOM height has changed, re-measure bodies and re-position
401 // their headers.
402 // This is separate from the typical JavaScript DOM change listeners because
403 // cases like NARROW_COLUMNS text reflow do not trigger DOM events.
404 mWebView.loadUrl("javascript:measurePositions();");
405 }
406 });
Andy Huangf70fc402012-02-17 15:37:42 -0800407
Andy Huang3233bff2012-03-20 19:38:45 -0700408 final WebSettings settings = mWebView.getSettings();
Andy Huangf70fc402012-02-17 15:37:42 -0800409
410 settings.setJavaScriptEnabled(true);
411 settings.setUseWideViewPort(true);
Andy Huang23014702012-07-09 12:50:36 -0700412 settings.setLoadWithOverviewMode(true);
Andy Huangf70fc402012-02-17 15:37:42 -0800413
414 settings.setSupportZoom(true);
415 settings.setBuiltInZoomControls(true);
416 settings.setDisplayZoomControls(false);
Mindy Pereira9b875682012-02-15 18:10:54 -0800417
Andy Huangc319b552012-04-25 19:53:50 -0700418 final float fontScale = getResources().getConfiguration().fontScale;
Andy Huangba283732012-06-25 19:14:10 -0700419 final int desiredFontSizePx = getResources()
420 .getInteger(R.integer.conversation_desired_font_size_px);
421 final int unstyledFontSizePx = getResources()
422 .getInteger(R.integer.conversation_unstyled_font_size_px);
Andy Huangc319b552012-04-25 19:53:50 -0700423
Andy Huangba283732012-06-25 19:14:10 -0700424 int textZoom = settings.getTextZoom();
425 // apply a correction to the default body text style to get regular text to the size we want
426 textZoom = textZoom * desiredFontSizePx / unstyledFontSizePx;
427 // then apply any system font scaling
Andy Huangc319b552012-04-25 19:53:50 -0700428 textZoom = (int) (textZoom * fontScale);
429 settings.setTextZoom(textZoom);
430
Andy Huang51067132012-03-12 20:08:19 -0700431 mViewsCreated = true;
432
Mindy Pereira9b875682012-02-15 18:10:54 -0800433 return rootView;
434 }
435
436 @Override
Andy Huanga6e965e2012-08-21 13:44:34 -0700437 public void onResume() {
438 super.onResume();
439
440 // Hacky workaround for http://b/6946182
441 Utils.fixSubTreeLayoutIfOrphaned(getView(), "ConversationViewFragment");
442 }
443
444 @Override
Andy Huang839ada22012-07-20 15:48:40 -0700445 public void onSaveInstanceState(Bundle outState) {
446 if (mViewState != null) {
447 outState.putParcelable(BUNDLE_VIEW_STATE, mViewState);
448 }
449 }
450
451 @Override
Mindy Pereira9b875682012-02-15 18:10:54 -0800452 public void onDestroyView() {
Mindy Pereira9b875682012-02-15 18:10:54 -0800453 super.onDestroyView();
Andy Huang46dfba62012-04-19 01:47:32 -0700454 mConversationContainer.setOverlayAdapter(null);
455 mAdapter = null;
Andy Huangc11011d2012-07-24 18:50:34 -0700456 if (mMarkReadObserver != null) {
457 mActivity.getConversationUpdater().unregisterConversationListObserver(
458 mMarkReadObserver);
459 mMarkReadObserver = null;
460 }
Andy Huang51067132012-03-12 20:08:19 -0700461 mViewsCreated = false;
Vikram Aggarwal7c401b72012-08-13 16:43:47 -0700462 mAccountObserver.unregisterAndDestroy();
Mindy Pereira9b875682012-02-15 18:10:54 -0800463 }
464
Andy Huang5ff63742012-03-16 20:30:23 -0700465 @Override
466 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
467 super.onCreateOptionsMenu(menu, inflater);
468
Vikram Aggarwald503df42012-05-11 10:13:35 -0700469 mChangeFoldersMenuItem = menu.findItem(R.id.change_folder);
Andy Huang5ff63742012-03-16 20:30:23 -0700470 }
471
Andy Huang5a929072012-03-23 20:17:10 -0700472 @Override
Mindy Pereirac9d59182012-03-22 16:06:46 -0700473 public void onPrepareOptionsMenu(Menu menu) {
474 super.onPrepareOptionsMenu(menu);
Paul Westbrook76b20622012-07-12 11:45:43 -0700475 final boolean showMarkImportant = !mConversation.isImportant();
Andy Huang991f4532012-08-14 13:32:55 -0700476 Utils.setMenuItemVisibility(menu, R.id.mark_important, showMarkImportant
477 && mAccount.supportsCapability(UIProvider.AccountCapabilities.MARK_IMPORTANT));
478 Utils.setMenuItemVisibility(menu, R.id.mark_not_important, !showMarkImportant
479 && mAccount.supportsCapability(UIProvider.AccountCapabilities.MARK_IMPORTANT));
Paul Westbrookef362542012-08-27 14:53:32 -0700480 final boolean showDelete = mFolder != null &&
481 mFolder.supportsCapability(UIProvider.FolderCapabilities.DELETE);
482 Utils.setMenuItemVisibility(menu, R.id.delete, showDelete);
483 // We only want to show the discard drafts menu item if we are not showing the delete menu
484 // item, and the current folder is a draft folder and the account supports discarding
485 // drafts for a conversation
486 final boolean showDiscardDrafts = !showDelete && mFolder != null && mFolder.isDraft() &&
487 mAccount.supportsCapability(AccountCapabilities.DISCARD_CONVERSATION_DRAFTS);
488 Utils.setMenuItemVisibility(menu, R.id.discard_drafts, showDiscardDrafts);
Andy Huang991f4532012-08-14 13:32:55 -0700489 final boolean archiveVisible = mAccount.supportsCapability(AccountCapabilities.ARCHIVE)
Mindy Pereirab68e4ae2012-08-17 09:23:44 -0700490 && mFolder != null && mFolder.supportsCapability(FolderCapabilities.ARCHIVE)
491 && !mFolder.isTrash();
Andy Huang991f4532012-08-14 13:32:55 -0700492 Utils.setMenuItemVisibility(menu, R.id.archive, archiveVisible);
Mindy Pereira01f30502012-08-14 10:30:51 -0700493 Utils.setMenuItemVisibility(menu, R.id.remove_folder, !archiveVisible && mFolder != null
Mindy Pereirab68e4ae2012-08-17 09:23:44 -0700494 && mFolder.supportsCapability(FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
495 && !mFolder.isProviderFolder());
Andy Huang7bf339a2012-08-14 14:34:43 -0700496 final MenuItem removeFolder = menu.findItem(R.id.remove_folder);
497 if (removeFolder != null) {
498 removeFolder.setTitle(getString(R.string.remove_folder, mFolder.name));
499 }
Mindy Pereira863e4412012-03-23 12:21:38 -0700500 Utils.setMenuItemVisibility(menu, R.id.report_spam,
501 mAccount.supportsCapability(AccountCapabilities.REPORT_SPAM) && mFolder != null
502 && mFolder.supportsCapability(FolderCapabilities.REPORT_SPAM)
503 && !mConversation.spam);
Paul Westbrook77eee622012-07-10 13:41:57 -0700504 Utils.setMenuItemVisibility(menu, R.id.mark_not_spam,
505 mAccount.supportsCapability(AccountCapabilities.REPORT_SPAM) && mFolder != null
506 && mFolder.supportsCapability(FolderCapabilities.MARK_NOT_SPAM)
507 && mConversation.spam);
Paul Westbrook76b20622012-07-12 11:45:43 -0700508 Utils.setMenuItemVisibility(menu, R.id.report_phishing,
509 mAccount.supportsCapability(AccountCapabilities.REPORT_PHISHING) && mFolder != null
510 && mFolder.supportsCapability(FolderCapabilities.REPORT_PHISHING)
511 && !mConversation.phishing);
Andy Huang991f4532012-08-14 13:32:55 -0700512 Utils.setMenuItemVisibility(menu, R.id.mute,
513 mAccount.supportsCapability(AccountCapabilities.MUTE) && mFolder != null
Mindy Pereira863e4412012-03-23 12:21:38 -0700514 && mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE)
515 && !mConversation.muted);
Mindy Pereirac9d59182012-03-22 16:06:46 -0700516 }
Andy Huang632721e2012-04-11 16:57:26 -0700517
Andy Huang839ada22012-07-20 15:48:40 -0700518 @Override
519 public boolean onOptionsItemSelected(MenuItem item) {
520 boolean handled = false;
521
522 switch (item.getItemId()) {
523 case R.id.inside_conversation_unread:
524 markUnread();
525 handled = true;
526 break;
527 }
528
529 return handled;
530 }
531
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700532 @Override
533 public ConversationUpdater getListController() {
534 final ControllableActivity activity = (ControllableActivity) getActivity();
535 return activity != null ? activity.getConversationUpdater() : null;
536 }
537
538 @Override
539 public MessageCursor getMessageCursor() {
540 return mCursor;
541 }
542
Andy Huang839ada22012-07-20 15:48:40 -0700543 private void markUnread() {
544 // Ignore unsafe calls made after a fragment is detached from an activity
545 final ControllableActivity activity = (ControllableActivity) getActivity();
546 if (activity == null) {
547 LogUtils.w(LOG_TAG, "ignoring markUnread for conv=%s", mConversation.id);
548 return;
549 }
550
Andy Huang28e31e22012-07-26 16:33:15 -0700551 if (mViewState == null) {
552 LogUtils.i(LOG_TAG, "ignoring markUnread for conv with no view state (%d)",
553 mConversation.id);
554 return;
555 }
Andy Huang839ada22012-07-20 15:48:40 -0700556 activity.getConversationUpdater().markConversationMessagesUnread(mConversation,
Vikram Aggarwal4a878b62012-07-31 15:09:25 -0700557 mViewState.getUnreadMessageUris(), mViewState.getConversationInfo());
Andy Huang839ada22012-07-20 15:48:40 -0700558 }
559
Andy Huang632721e2012-04-11 16:57:26 -0700560 /**
561 * {@link #setUserVisibleHint(boolean)} only works on API >= 15, so implement our own for
562 * reliability on older platforms.
563 */
564 public void setExtraUserVisibleHint(boolean isVisibleToUser) {
565 LogUtils.v(LOG_TAG, "in CVF.setHint, val=%s (%s)", isVisibleToUser, this);
566
567 if (mUserVisible != isVisibleToUser) {
568 mUserVisible = isVisibleToUser;
569
570 if (isVisibleToUser && mViewsCreated) {
571
572 if (mCursor == null && mDeferredConversationLoad) {
573 // load
574 LogUtils.v(LOG_TAG, "Fragment is now user-visible, showing conversation: %s",
575 mConversation.uri);
576 showConversation();
577 mDeferredConversationLoad = false;
578 } else {
579 onConversationSeen();
580 }
581
582 }
583 }
584 }
585
Mindy Pereira9b875682012-02-15 18:10:54 -0800586 /**
mindyp3bcf1802012-09-09 11:17:00 -0700587 * Handles a request to show a new conversation list, either from a search
588 * query or for viewing a folder. This will initiate a data load, and hence
589 * must be called on the UI thread.
Mindy Pereira9b875682012-02-15 18:10:54 -0800590 */
591 private void showConversation() {
mindyp3bcf1802012-09-09 11:17:00 -0700592 final boolean disableOffscreenLoading = DISABLE_OFFSCREEN_LOADING
593 || (mConversation.isRemote
594 || mConversation.getNumMessages() > mMaxAutoLoadMessages);
Andy Huang47aa9c92012-07-31 15:37:21 -0700595 if (!mUserVisible && disableOffscreenLoading) {
Andy Huang632721e2012-04-11 16:57:26 -0700596 LogUtils.v(LOG_TAG, "Fragment not user-visible, not showing conversation: %s",
597 mConversation.uri);
598 mDeferredConversationLoad = true;
599 return;
600 }
601 LogUtils.v(LOG_TAG,
602 "Fragment is short or user-visible, immediately rendering conversation: %s",
603 mConversation.uri);
mindyp3bcf1802012-09-09 11:17:00 -0700604 mWebView.setVisibility(View.VISIBLE);
Andy Huangb8331b42012-07-16 19:08:53 -0700605 getLoaderManager().initLoader(MESSAGE_LOADER_ID, Bundle.EMPTY, mMessageLoaderCallbacks);
Andy Huang5460ce42012-08-16 19:38:27 -0700606 if (mUserVisible) {
607 final SubjectDisplayChanger sdc = mActivity.getSubjectDisplayChanger();
608 if (sdc != null) {
609 sdc.setSubject(mConversation.subject);
610 }
611 }
mindyp3bcf1802012-09-09 11:17:00 -0700612 // TODO(mindyp): don't show loading status for a previously rendered
613 // conversation. Ielieve this is better done by making sure don't show loading status
614 // until XX ms have passed without loading completed.
615 showLoadingStatus();
Mindy Pereira8e915722012-02-16 14:42:56 -0800616 }
617
Andy Huang632721e2012-04-11 16:57:26 -0700618 public Conversation getConversation() {
619 return mConversation;
620 }
621
Andy Huang51067132012-03-12 20:08:19 -0700622 private void renderConversation(MessageCursor messageCursor) {
mindyp3bcf1802012-09-09 11:17:00 -0700623 final String convHtml = renderMessageBodies(messageCursor, mEnableContentReadySignal);
Andy Huangbd544e32012-05-29 15:56:51 -0700624
625 if (DEBUG_DUMP_CONVERSATION_HTML) {
626 java.io.FileWriter fw = null;
627 try {
628 fw = new java.io.FileWriter("/sdcard/conv" + mConversation.id
629 + ".html");
630 fw.write(convHtml);
631 } catch (java.io.IOException e) {
632 e.printStackTrace();
633 } finally {
634 if (fw != null) {
635 try {
636 fw.close();
637 } catch (java.io.IOException e) {
638 e.printStackTrace();
639 }
640 }
641 }
642 }
643
644 mWebView.loadDataWithBaseURL(mBaseUri, convHtml, "text/html", "utf-8", null);
Andy Huang7bdc3752012-03-25 17:18:19 -0700645 mCursor = messageCursor;
Andy Huang51067132012-03-12 20:08:19 -0700646 }
647
Andy Huang7bdc3752012-03-25 17:18:19 -0700648 /**
649 * Populate the adapter with overlay views (message headers, super-collapsed blocks, a
650 * conversation header), and return an HTML document with spacer divs inserted for all overlays.
651 *
652 */
mindyp3bcf1802012-09-09 11:17:00 -0700653 private String renderMessageBodies(MessageCursor messageCursor,
654 boolean enableContentReadySignal) {
Andy Huangf70fc402012-02-17 15:37:42 -0800655 int pos = -1;
Andy Huang632721e2012-04-11 16:57:26 -0700656
Andy Huang1ee96b22012-08-24 20:19:53 -0700657 LogUtils.d(LOG_TAG, "IN renderMessageBodies, fragment=%s", this);
Andy Huang7bdc3752012-03-25 17:18:19 -0700658 boolean allowNetworkImages = false;
659
Andy Huangc7543572012-04-03 15:34:29 -0700660 // TODO: re-use any existing adapter item state (expanded, details expanded, show pics)
Andy Huang28b7aee2012-08-20 20:27:32 -0700661
Andy Huang7bdc3752012-03-25 17:18:19 -0700662 // Walk through the cursor and build up an overlay adapter as you go.
663 // Each overlay has an entry in the adapter for easy scroll handling in the container.
664 // Items are not necessarily 1:1 in cursor and adapter because of super-collapsed blocks.
665 // When adding adapter items, also add their heights to help the container later determine
666 // overlay dimensions.
667
Andy Huangdb620fe2012-08-24 15:45:28 -0700668 // When re-rendering, prevent ConversationContainer from laying out overlays until after
669 // the new spacers are positioned by WebView.
670 mConversationContainer.invalidateSpacerGeometry();
671
Andy Huang7bdc3752012-03-25 17:18:19 -0700672 mAdapter.clear();
673
Andy Huang47aa9c92012-07-31 15:37:21 -0700674 // re-evaluate the message parts of the view state, since the messages may have changed
675 // since the previous render
676 final ConversationViewState prevState = mViewState;
677 mViewState = new ConversationViewState(prevState);
678
Andy Huang5ff63742012-03-16 20:30:23 -0700679 // N.B. the units of height for spacers are actually dp and not px because WebView assumes
Andy Huang2e9acfe2012-03-15 22:39:36 -0700680 // a pixel is an mdpi pixel, unless you set device-dpi.
Andy Huang5ff63742012-03-16 20:30:23 -0700681
Andy Huang7bdc3752012-03-25 17:18:19 -0700682 // add a single conversation header item
683 final int convHeaderPos = mAdapter.addConversationHeader(mConversation);
Andy Huang23014702012-07-09 12:50:36 -0700684 final int convHeaderPx = measureOverlayHeight(convHeaderPos);
Andy Huang5ff63742012-03-16 20:30:23 -0700685
Andy Huang256b35c2012-08-22 15:19:13 -0700686 final int sideMarginPx = getResources().getDimensionPixelOffset(
687 R.dimen.conversation_view_margin_side) + getResources().getDimensionPixelOffset(
688 R.dimen.conversation_message_content_margin_side);
689
690 mTemplates.startConversation(mWebView.screenPxToWebPx(sideMarginPx),
691 mWebView.screenPxToWebPx(convHeaderPx));
Andy Huang3233bff2012-03-20 19:38:45 -0700692
Andy Huang46dfba62012-04-19 01:47:32 -0700693 int collapsedStart = -1;
Andy Huang839ada22012-07-20 15:48:40 -0700694 ConversationMessage prevCollapsedMsg = null;
Andy Huang46dfba62012-04-19 01:47:32 -0700695 boolean prevSafeForImages = false;
696
Andy Huangf70fc402012-02-17 15:37:42 -0800697 while (messageCursor.moveToPosition(++pos)) {
Andy Huang839ada22012-07-20 15:48:40 -0700698 final ConversationMessage msg = messageCursor.getMessage();
Andy Huang46dfba62012-04-19 01:47:32 -0700699
Andy Huang3233bff2012-03-20 19:38:45 -0700700 // TODO: save/restore 'show pics' state
701 final boolean safeForImages = msg.alwaysShowImages /* || savedStateSaysSafe */;
702 allowNetworkImages |= safeForImages;
Andy Huang24055282012-03-27 17:37:06 -0700703
Paul Westbrook08098ec2012-08-12 15:30:28 -0700704 final Integer savedExpanded = prevState.getExpansionState(msg);
705 final int expandedState;
Andy Huang839ada22012-07-20 15:48:40 -0700706 if (savedExpanded != null) {
Andy Huang1ee96b22012-08-24 20:19:53 -0700707 if (ExpansionState.isSuperCollapsed(savedExpanded) && messageCursor.isLast()) {
708 // override saved state when this is now the new last message
709 // this happens to the second-to-last message when you discard a draft
710 expandedState = ExpansionState.EXPANDED;
711 } else {
712 expandedState = savedExpanded;
713 }
Andy Huang839ada22012-07-20 15:48:40 -0700714 } else {
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700715 // new messages that are not expanded default to being eligible for super-collapse
Paul Westbrook08098ec2012-08-12 15:30:28 -0700716 expandedState = (!msg.read || msg.starred || messageCursor.isLast()) ?
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700717 ExpansionState.EXPANDED : ExpansionState.SUPER_COLLAPSED;
Andy Huang839ada22012-07-20 15:48:40 -0700718 }
Paul Westbrook08098ec2012-08-12 15:30:28 -0700719 mViewState.setExpansionState(msg, expandedState);
Andy Huangc7543572012-04-03 15:34:29 -0700720
Andy Huang839ada22012-07-20 15:48:40 -0700721 // save off "read" state from the cursor
722 // later, the view may not match the cursor (e.g. conversation marked read on open)
Andy Huang423bea22012-08-21 12:00:49 -0700723 // however, if a previous state indicated this message was unread, trust that instead
724 // so "mark unread" marks all originally unread messages
725 mViewState.setReadState(msg, msg.read && !prevState.isUnread(msg));
Andy Huang839ada22012-07-20 15:48:40 -0700726
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700727 // We only want to consider this for inclusion in the super collapsed block if
728 // 1) The we don't have previous state about this message (The first time that the
729 // user opens a conversation)
730 // 2) The previously saved state for this message indicates that this message is
731 // in the super collapsed block.
732 if (ExpansionState.isSuperCollapsed(expandedState)) {
733 // contribute to a super-collapsed block that will be emitted just before the
734 // next expanded header
735 if (collapsedStart < 0) {
736 collapsedStart = pos;
Andy Huang46dfba62012-04-19 01:47:32 -0700737 }
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700738 prevCollapsedMsg = msg;
739 prevSafeForImages = safeForImages;
740 continue;
Andy Huang46dfba62012-04-19 01:47:32 -0700741 }
Andy Huang24055282012-03-27 17:37:06 -0700742
Andy Huang46dfba62012-04-19 01:47:32 -0700743 // resolve any deferred decisions on previous collapsed items
744 if (collapsedStart >= 0) {
745 if (pos - collapsedStart == 1) {
746 // special-case for a single collapsed message: no need to super-collapse it
747 renderMessage(prevCollapsedMsg, false /* expanded */,
748 prevSafeForImages);
749 } else {
750 renderSuperCollapsedBlock(collapsedStart, pos - 1);
751 }
752 prevCollapsedMsg = null;
753 collapsedStart = -1;
754 }
Andy Huang7bdc3752012-03-25 17:18:19 -0700755
Paul Westbrook08098ec2012-08-12 15:30:28 -0700756 renderMessage(msg, ExpansionState.isExpanded(expandedState), safeForImages);
Mindy Pereira9b875682012-02-15 18:10:54 -0800757 }
Andy Huang3233bff2012-03-20 19:38:45 -0700758
759 mWebView.getSettings().setBlockNetworkImage(!allowNetworkImages);
760
Paul Westbrookcebcc642012-08-08 10:06:04 -0700761 // If the conversation has specified a base uri, use it here, use mBaseUri
762 final String conversationBaseUri = mConversation.conversationBaseUri != null ?
763 mConversation.conversationBaseUri.toString() : mBaseUri;
764 return mTemplates.endConversation(mBaseUri, conversationBaseUri, 320,
mindyp3bcf1802012-09-09 11:17:00 -0700765 mWebView.getViewportWidth(), enableContentReadySignal);
Mindy Pereira9b875682012-02-15 18:10:54 -0800766 }
Mindy Pereira674afa42012-02-17 14:05:24 -0800767
Andy Huang46dfba62012-04-19 01:47:32 -0700768 private void renderSuperCollapsedBlock(int start, int end) {
769 final int blockPos = mAdapter.addSuperCollapsedBlock(start, end);
Andy Huang23014702012-07-09 12:50:36 -0700770 final int blockPx = measureOverlayHeight(blockPos);
771 mTemplates.appendSuperCollapsedHtml(start, mWebView.screenPxToWebPx(blockPx));
Andy Huang46dfba62012-04-19 01:47:32 -0700772 }
773
Andy Huang839ada22012-07-20 15:48:40 -0700774 private void renderMessage(ConversationMessage msg, boolean expanded,
775 boolean safeForImages) {
Andy Huang46dfba62012-04-19 01:47:32 -0700776 final int headerPos = mAdapter.addMessageHeader(msg, expanded);
777 final MessageHeaderItem headerItem = (MessageHeaderItem) mAdapter.getItem(headerPos);
778
779 final int footerPos = mAdapter.addMessageFooter(headerItem);
780
781 // Measure item header and footer heights to allocate spacers in HTML
782 // But since the views themselves don't exist yet, render each item temporarily into
783 // a host view for measurement.
Andy Huang23014702012-07-09 12:50:36 -0700784 final int headerPx = measureOverlayHeight(headerPos);
785 final int footerPx = measureOverlayHeight(footerPos);
Andy Huang46dfba62012-04-19 01:47:32 -0700786
Andy Huang256b35c2012-08-22 15:19:13 -0700787 mTemplates.appendMessageHtml(msg, expanded, safeForImages,
Andy Huang23014702012-07-09 12:50:36 -0700788 mWebView.screenPxToWebPx(headerPx), mWebView.screenPxToWebPx(footerPx));
Andy Huang46dfba62012-04-19 01:47:32 -0700789 }
790
791 private String renderCollapsedHeaders(MessageCursor cursor,
792 SuperCollapsedBlockItem blockToReplace) {
793 final List<ConversationOverlayItem> replacements = Lists.newArrayList();
794
795 mTemplates.reset();
796
797 for (int i = blockToReplace.getStart(), end = blockToReplace.getEnd(); i <= end; i++) {
798 cursor.moveToPosition(i);
Andy Huang839ada22012-07-20 15:48:40 -0700799 final ConversationMessage msg = cursor.getMessage();
Andy Huang46dfba62012-04-19 01:47:32 -0700800 final MessageHeaderItem header = mAdapter.newMessageHeaderItem(msg,
801 false /* expanded */);
802 final MessageFooterItem footer = mAdapter.newMessageFooterItem(header);
803
Andy Huang23014702012-07-09 12:50:36 -0700804 final int headerPx = measureOverlayHeight(header);
805 final int footerPx = measureOverlayHeight(footer);
Andy Huang46dfba62012-04-19 01:47:32 -0700806
Andy Huang256b35c2012-08-22 15:19:13 -0700807 mTemplates.appendMessageHtml(msg, false /* expanded */, msg.alwaysShowImages,
Andy Huang23014702012-07-09 12:50:36 -0700808 mWebView.screenPxToWebPx(headerPx), mWebView.screenPxToWebPx(footerPx));
Andy Huang46dfba62012-04-19 01:47:32 -0700809 replacements.add(header);
810 replacements.add(footer);
Andy Huang839ada22012-07-20 15:48:40 -0700811
Paul Westbrook08098ec2012-08-12 15:30:28 -0700812 mViewState.setExpansionState(msg, ExpansionState.COLLAPSED);
Andy Huang46dfba62012-04-19 01:47:32 -0700813 }
814
815 mAdapter.replaceSuperCollapsedBlock(blockToReplace, replacements);
816
817 return mTemplates.emit();
818 }
819
820 private int measureOverlayHeight(int position) {
821 return measureOverlayHeight(mAdapter.getItem(position));
822 }
823
Andy Huang7bdc3752012-03-25 17:18:19 -0700824 /**
Andy Huangb8331b42012-07-16 19:08:53 -0700825 * Measure the height of an adapter view by rendering an adapter item into a temporary
Andy Huang46dfba62012-04-19 01:47:32 -0700826 * host view, and asking the view to immediately measure itself. This method will reuse
Andy Huang7bdc3752012-03-25 17:18:19 -0700827 * a previous adapter view from {@link ConversationContainer}'s scrap views if one was generated
828 * earlier.
829 * <p>
Andy Huang46dfba62012-04-19 01:47:32 -0700830 * After measuring the height, this method also saves the height in the
831 * {@link ConversationOverlayItem} for later use in overlay positioning.
Andy Huang7bdc3752012-03-25 17:18:19 -0700832 *
Andy Huang46dfba62012-04-19 01:47:32 -0700833 * @param convItem adapter item with data to render and measure
Andy Huang23014702012-07-09 12:50:36 -0700834 * @return height of the rendered view in screen px
Andy Huang7bdc3752012-03-25 17:18:19 -0700835 */
Andy Huang46dfba62012-04-19 01:47:32 -0700836 private int measureOverlayHeight(ConversationOverlayItem convItem) {
Andy Huang7bdc3752012-03-25 17:18:19 -0700837 final int type = convItem.getType();
838
839 final View convertView = mConversationContainer.getScrapView(type);
Andy Huangb8331b42012-07-16 19:08:53 -0700840 final View hostView = mAdapter.getView(convItem, convertView, mConversationContainer,
841 true /* measureOnly */);
Andy Huang7bdc3752012-03-25 17:18:19 -0700842 if (convertView == null) {
843 mConversationContainer.addScrapView(type, hostView);
844 }
845
Andy Huang9875bb42012-04-04 20:36:21 -0700846 final int heightPx = mConversationContainer.measureOverlay(hostView);
Andy Huang7bdc3752012-03-25 17:18:19 -0700847 convItem.setHeight(heightPx);
Andy Huang9875bb42012-04-04 20:36:21 -0700848 convItem.markMeasurementValid();
Andy Huang7bdc3752012-03-25 17:18:19 -0700849
Andy Huang23014702012-07-09 12:50:36 -0700850 return heightPx;
Andy Huang7bdc3752012-03-25 17:18:19 -0700851 }
852
Andy Huang632721e2012-04-11 16:57:26 -0700853 private void onConversationSeen() {
Andy Huang5895f7b2012-06-01 17:07:20 -0700854 // Ignore unsafe calls made after a fragment is detached from an activity
855 final ControllableActivity activity = (ControllableActivity) getActivity();
856 if (activity == null) {
857 LogUtils.w(LOG_TAG, "ignoring onConversationSeen for conv=%s", mConversation.id);
858 return;
859 }
860
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700861 mViewState.setInfoForConversation(mConversation);
Andy Huangc11011d2012-07-24 18:50:34 -0700862
Andy Huang423bea22012-08-21 12:00:49 -0700863 // mark viewed/read if not previously marked viewed by this conversation view,
864 // or if unread messages still exist in the message list cursor
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700865 // we don't want to keep marking viewed on rotation or restore
Andy Huang423bea22012-08-21 12:00:49 -0700866 // but we do want future re-renders to mark read (e.g. "New message from X" case)
867 if (!mConversation.isViewed() || (mCursor != null && !mCursor.isConversationRead())) {
Vikram Aggarwal66bc2aa2012-08-02 10:47:03 -0700868 final ConversationUpdater listController = activity.getConversationUpdater();
869 // The conversation cursor may not have finished loading by now (when launched via
870 // notification), so watch for when it finishes and mark it read then.
871 if (listController.getConversationListCursor() == null) {
872 LogUtils.i(LOG_TAG, "deferring conv mark read on open for id=%d",
873 mConversation.id);
874 mMarkReadObserver = new MarkReadObserver(listController);
875 listController.registerConversationListObserver(mMarkReadObserver);
876 } else {
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700877 markReadOnSeen(listController);
Andy Huang7e854f52012-07-24 11:35:49 -0700878 }
Andy Huang632721e2012-04-11 16:57:26 -0700879 }
880
Andy Huang3825f3d2012-08-29 16:44:12 -0700881 activity.getListHandler().onConversationSeen(mConversation);
Andy Huang632721e2012-04-11 16:57:26 -0700882 }
883
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700884 private void markReadOnSeen(ConversationUpdater listController) {
885 // Mark the conversation viewed and read.
886 listController.markConversationsRead(Arrays.asList(mConversation), true /* read */,
887 true /* viewed */);
888
889 // and update the Message objects in the cursor so the next time a cursor update happens
890 // with these messages marked read, we know to ignore it
891 if (mCursor != null) {
892 mCursor.markMessagesRead();
893 }
894 }
895
Andy Huang3233bff2012-03-20 19:38:45 -0700896 // BEGIN conversation header callbacks
Andy Huang5ff63742012-03-16 20:30:23 -0700897 @Override
898 public void onFoldersClicked() {
899 if (mChangeFoldersMenuItem == null) {
900 LogUtils.e(LOG_TAG, "unable to open 'change folders' dialog for a conversation");
901 return;
902 }
903 mActivity.onOptionsItemSelected(mChangeFoldersMenuItem);
904 }
905
906 @Override
907 public void onConversationViewHeaderHeightChange(int newHeight) {
908 // TODO: propagate the new height to the header's HTML spacer. This can happen when labels
909 // are added/removed
910 }
911
912 @Override
913 public String getSubjectRemainder(String subject) {
Andy Huang5895f7b2012-06-01 17:07:20 -0700914 final SubjectDisplayChanger sdc = mActivity.getSubjectDisplayChanger();
915 if (sdc == null) {
916 return subject;
917 }
918 return sdc.getUnshownSubject(subject);
Andy Huang5ff63742012-03-16 20:30:23 -0700919 }
Andy Huang3233bff2012-03-20 19:38:45 -0700920 // END conversation header callbacks
921
922 // START message header callbacks
923 @Override
Andy Huangc7543572012-04-03 15:34:29 -0700924 public void setMessageSpacerHeight(MessageHeaderItem item, int newSpacerHeightPx) {
925 mConversationContainer.invalidateSpacerGeometry();
926
927 // update message HTML spacer height
Andy Huang23014702012-07-09 12:50:36 -0700928 final int h = mWebView.screenPxToWebPx(newSpacerHeightPx);
929 LogUtils.i(LAYOUT_TAG, "setting HTML spacer h=%dwebPx (%dscreenPx)", h,
930 newSpacerHeightPx);
Andy Huangc7543572012-04-03 15:34:29 -0700931 mWebView.loadUrl(String.format("javascript:setMessageHeaderSpacerHeight('%s', %d);",
Andy Huang23014702012-07-09 12:50:36 -0700932 mTemplates.getMessageDomId(item.message), h));
Andy Huang3233bff2012-03-20 19:38:45 -0700933 }
934
935 @Override
Andy Huangc7543572012-04-03 15:34:29 -0700936 public void setMessageExpanded(MessageHeaderItem item, int newSpacerHeightPx) {
937 mConversationContainer.invalidateSpacerGeometry();
938
939 // show/hide the HTML message body and update the spacer height
Andy Huang23014702012-07-09 12:50:36 -0700940 final int h = mWebView.screenPxToWebPx(newSpacerHeightPx);
941 LogUtils.i(LAYOUT_TAG, "setting HTML spacer expanded=%s h=%dwebPx (%dscreenPx)",
942 item.isExpanded(), h, newSpacerHeightPx);
Andy Huangc7543572012-04-03 15:34:29 -0700943 mWebView.loadUrl(String.format("javascript:setMessageBodyVisible('%s', %s, %d);",
Andy Huang23014702012-07-09 12:50:36 -0700944 mTemplates.getMessageDomId(item.message), item.isExpanded(), h));
Andy Huang839ada22012-07-20 15:48:40 -0700945
Paul Westbrook08098ec2012-08-12 15:30:28 -0700946 mViewState.setExpansionState(item.message,
947 item.isExpanded() ? ExpansionState.EXPANDED : ExpansionState.COLLAPSED);
Andy Huang3233bff2012-03-20 19:38:45 -0700948 }
949
950 @Override
951 public void showExternalResources(Message msg) {
952 mWebView.getSettings().setBlockNetworkImage(false);
953 mWebView.loadUrl("javascript:unblockImages('" + mTemplates.getMessageDomId(msg) + "');");
954 }
955 // END message header callbacks
Andy Huang5ff63742012-03-16 20:30:23 -0700956
Andy Huang46dfba62012-04-19 01:47:32 -0700957 @Override
958 public void onSuperCollapsedClick(SuperCollapsedBlockItem item) {
959 if (mCursor == null || !mViewsCreated) {
960 return;
961 }
962
963 mTempBodiesHtml = renderCollapsedHeaders(mCursor, item);
964 mWebView.loadUrl("javascript:replaceSuperCollapsedBlock(" + item.getStart() + ")");
965 }
966
Andy Huang47aa9c92012-07-31 15:37:21 -0700967 private void showNewMessageNotification(NewMessagesInfo info) {
968 final TextView descriptionView = (TextView) mNewMessageBar.findViewById(
969 R.id.new_message_description);
970 descriptionView.setText(info.getNotificationText());
971 mNewMessageBar.setVisibility(View.VISIBLE);
972 }
973
974 private void onNewMessageBarClick() {
975 mNewMessageBar.setVisibility(View.GONE);
976
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700977 renderConversation(mCursor); // mCursor is already up-to-date per onLoadFinished()
Andy Huang47aa9c92012-07-31 15:37:21 -0700978 }
979
Andy Huang5fbda022012-02-28 18:22:03 -0800980 private static class MessageLoader extends CursorLoader {
Paul Westbrook2d82d612012-03-07 09:21:14 -0800981 private boolean mDeliveredFirstResults = false;
Andy Huang839ada22012-07-20 15:48:40 -0700982 private final Conversation mConversation;
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700983 private final ConversationController mController;
Andy Huang5fbda022012-02-28 18:22:03 -0800984
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700985 public MessageLoader(Context c, Conversation conv, ConversationController controller) {
Andy Huang839ada22012-07-20 15:48:40 -0700986 super(c, conv.messageListUri, UIProvider.MESSAGE_PROJECTION, null, null, null);
987 mConversation = conv;
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700988 mController = controller;
Andy Huang5fbda022012-02-28 18:22:03 -0800989 }
990
991 @Override
992 public Cursor loadInBackground() {
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700993 return new MessageCursor(super.loadInBackground(), mConversation, mController);
Andy Huang5fbda022012-02-28 18:22:03 -0800994 }
Paul Westbrook2d82d612012-03-07 09:21:14 -0800995
996 @Override
997 public void deliverResult(Cursor result) {
998 // We want to deliver these results, and then we want to make sure that any subsequent
999 // queries do not hit the network
1000 super.deliverResult(result);
1001
1002 if (!mDeliveredFirstResults) {
1003 mDeliveredFirstResults = true;
1004 Uri uri = getUri();
1005
1006 // Create a ListParams that tells the provider to not hit the network
1007 final ListParams listParams =
1008 new ListParams(ListParams.NO_LIMIT, false /* useNetwork */);
1009
1010 // Build the new uri with this additional parameter
1011 uri = uri.buildUpon().appendQueryParameter(
1012 UIProvider.LIST_PARAMS_QUERY_PARAMETER, listParams.serialize()).build();
1013 setUri(uri);
1014 }
1015 }
Andy Huang5fbda022012-02-28 18:22:03 -08001016 }
1017
Andy Huangb5078b22012-03-05 19:52:29 -08001018 private static int[] parseInts(final String[] stringArray) {
1019 final int len = stringArray.length;
1020 final int[] ints = new int[len];
1021 for (int i = 0; i < len; i++) {
1022 ints[i] = Integer.parseInt(stringArray[i]);
1023 }
1024 return ints;
1025 }
1026
Andy Huang47aa9c92012-07-31 15:37:21 -07001027 @Override
1028 public String toString() {
1029 // log extra info at DEBUG level or finer
1030 final String s = super.toString();
1031 if (!LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG) || mConversation == null) {
1032 return s;
1033 }
1034 return "(" + s + " subj=" + mConversation.subject + ")";
1035 }
1036
Andy Huang16174812012-08-16 16:40:35 -07001037 private Address getAddress(String rawFrom) {
1038 Address addr = mAddressCache.get(rawFrom);
1039 if (addr == null) {
1040 addr = Address.getEmailAddress(rawFrom);
1041 mAddressCache.put(rawFrom, addr);
1042 }
1043 return addr;
1044 }
1045
Andy Huang28b7aee2012-08-20 20:27:32 -07001046 @Override
1047 public Account getAccount() {
1048 return mAccount;
1049 }
1050
Andy Huang17a9cde2012-03-09 18:03:16 -08001051 private class ConversationWebViewClient extends WebViewClient {
1052
1053 @Override
1054 public void onPageFinished(WebView view, String url) {
Andy Huangde56e972012-07-26 18:23:08 -07001055 // Ignore unsafe calls made after a fragment is detached from an activity
1056 final ControllableActivity activity = (ControllableActivity) getActivity();
1057 if (activity == null || !mViewsCreated) {
1058 LogUtils.i(LOG_TAG, "ignoring CVF.onPageFinished, url=%s fragment=%s", url,
Andy Huangb95da852012-07-18 14:16:58 -07001059 ConversationViewFragment.this);
1060 return;
1061 }
1062
Andy Huang47aa9c92012-07-31 15:37:21 -07001063 LogUtils.i(LOG_TAG, "IN CVF.onPageFinished, url=%s fragment=%s act=%s", url,
1064 ConversationViewFragment.this, getActivity());
Andy Huang632721e2012-04-11 16:57:26 -07001065
Andy Huang17a9cde2012-03-09 18:03:16 -08001066 super.onPageFinished(view, url);
1067
1068 // TODO: save off individual message unread state (here, or in onLoadFinished?) so
1069 // 'mark unread' restores the original unread state for each individual message
1070
Andy Huang632721e2012-04-11 16:57:26 -07001071 if (mUserVisible) {
1072 onConversationSeen();
Andy Huang51067132012-03-12 20:08:19 -07001073 }
mindyp3bcf1802012-09-09 11:17:00 -07001074 if (!mEnableContentReadySignal) {
1075 notifyConversationLoaded(mConversation);
1076 dismissLoadingStatus();
1077 }
Andy Huangb8331b42012-07-16 19:08:53 -07001078 final Set<String> emailAddresses = Sets.newHashSet();
1079 for (Address addr : mAddressCache.values()) {
1080 emailAddresses.add(addr.getAddress());
1081 }
1082 mContactLoaderCallbacks.setSenders(emailAddresses);
1083 getLoaderManager().restartLoader(CONTACT_LOADER_ID, Bundle.EMPTY,
1084 mContactLoaderCallbacks);
Andy Huang17a9cde2012-03-09 18:03:16 -08001085 }
1086
Andy Huangaf5d4e02012-03-19 19:02:12 -07001087 @Override
1088 public boolean shouldOverrideUrlLoading(WebView view, String url) {
Andy Huang46dfba62012-04-19 01:47:32 -07001089 final Activity activity = getActivity();
1090 if (!mViewsCreated || activity == null) {
1091 return false;
1092 }
1093
Andy Huangaf5d4e02012-03-19 19:02:12 -07001094 boolean result = false;
Mark Wei9982fdb2012-08-30 18:27:46 -07001095 final Intent intent;
1096 Uri uri = Uri.parse(url);
1097 if (!Utils.isEmpty(mAccount.viewIntentProxyUri)) {
1098 intent = new Intent(Intent.ACTION_VIEW, mAccount.viewIntentProxyUri);
1099 intent.putExtra(ViewProxyExtras.EXTRA_ORIGINAL_URI, uri);
1100 intent.putExtra(ViewProxyExtras.EXTRA_ACCOUNT, mAccount);
1101 } else {
1102 intent = new Intent(Intent.ACTION_VIEW, uri);
1103 intent.putExtra(Browser.EXTRA_APPLICATION_ID, activity.getPackageName());
1104 }
Andy Huangaf5d4e02012-03-19 19:02:12 -07001105
1106 try {
Mark Wei9982fdb2012-08-30 18:27:46 -07001107 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
Andy Huang46dfba62012-04-19 01:47:32 -07001108 activity.startActivity(intent);
Andy Huangaf5d4e02012-03-19 19:02:12 -07001109 result = true;
1110 } catch (ActivityNotFoundException ex) {
1111 // If no application can handle the URL, assume that the
1112 // caller can handle it.
1113 }
1114
1115 return result;
1116 }
1117
Andy Huang17a9cde2012-03-09 18:03:16 -08001118 }
1119
Andy Huangf70fc402012-02-17 15:37:42 -08001120 /**
mindyp3bcf1802012-09-09 11:17:00 -07001121 * Notifies the {@link ConversationViewable.ConversationCallbacks} that the conversation has
1122 * been loaded.
1123 */
1124 public void notifyConversationLoaded(Conversation c) {
1125 // Do nothing.
1126 }
1127
1128 /**
1129 * Notifies the {@link ConversationViewable.ConversationCallbacks} that the conversation has
1130 * failed to load.
1131 */
1132 protected void notifyConversationLoadError(Conversation c) {
1133 mActivity.onConversationLoadError();
1134 }
1135
1136 private void showLoadingStatus() {
1137 mBackgroundView.setVisibility(View.VISIBLE);
1138 String senders = mConversation.getSenders(mContext);
1139 if (!TextUtils.isEmpty(senders) && mConversation.subject != null) {
1140 mInfoView.setVisibility(View.VISIBLE);
1141 mSendersView.setText(senders);
1142 mSubjectView.setText(createSubjectSnippet(mConversation.subject,
1143 mConversation.getSnippet()));
1144 } else {
1145 mProgressView.setVisibility(View.VISIBLE);
1146 }
1147 }
1148
1149 private void dismissLoadingStatus() {
1150 // Fade out the info view.
1151 if (mBackgroundView.getVisibility() == View.VISIBLE) {
1152 Animator animator = AnimatorInflater.loadAnimator(mContext, R.anim.fade_out);
1153 animator.setTarget(mBackgroundView);
1154 animator.addListener(new AnimatorListener() {
1155 @Override
1156 public void onAnimationStart(Animator animation) {
1157 if (mProgressView.getVisibility() != View.VISIBLE) {
1158 mProgressView.setVisibility(View.GONE);
1159 }
1160 }
1161
1162 @Override
1163 public void onAnimationEnd(Animator animation) {
1164 mBackgroundView.setVisibility(View.GONE);
1165 mInfoView.setVisibility(View.GONE);
1166 mProgressView.setVisibility(View.GONE);
1167 }
1168
1169 @Override
1170 public void onAnimationCancel(Animator animation) {
1171 // Do nothing.
1172 }
1173
1174 @Override
1175 public void onAnimationRepeat(Animator animation) {
1176 // Do nothing.
1177 }
1178 });
1179 animator.start();
1180 } else {
1181 mBackgroundView.setVisibility(View.GONE);
1182 mInfoView.setVisibility(View.GONE);
1183 mProgressView.setVisibility(View.GONE);
1184 }
1185 }
1186
1187 /**
Andy Huangf70fc402012-02-17 15:37:42 -08001188 * NOTE: all public methods must be listed in the proguard flags so that they can be accessed
1189 * via reflection and not stripped.
1190 *
1191 */
1192 private class MailJsBridge {
1193
1194 @SuppressWarnings("unused")
Andy Huang7bdc3752012-03-25 17:18:19 -07001195 public void onWebContentGeometryChange(final String[] overlayBottomStrs) {
Andy Huang46dfba62012-04-19 01:47:32 -07001196 try {
1197 mHandler.post(new Runnable() {
1198 @Override
1199 public void run() {
1200 if (!mViewsCreated) {
1201 LogUtils.d(LOG_TAG, "ignoring webContentGeometryChange because views" +
1202 " are gone, %s", ConversationViewFragment.this);
1203 return;
1204 }
Andy Huang51067132012-03-12 20:08:19 -07001205
Andy Huang46dfba62012-04-19 01:47:32 -07001206 mConversationContainer.onGeometryChange(parseInts(overlayBottomStrs));
1207 }
1208 });
1209 } catch (Throwable t) {
1210 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onWebContentGeometryChange");
1211 }
1212 }
1213
1214 @SuppressWarnings("unused")
1215 public String getTempMessageBodies() {
1216 try {
1217 if (!mViewsCreated) {
1218 return "";
Andy Huangf70fc402012-02-17 15:37:42 -08001219 }
Andy Huang46dfba62012-04-19 01:47:32 -07001220
1221 final String s = mTempBodiesHtml;
1222 mTempBodiesHtml = null;
1223 return s;
1224 } catch (Throwable t) {
1225 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getTempMessageBodies");
1226 return "";
1227 }
Andy Huangf70fc402012-02-17 15:37:42 -08001228 }
1229
mindyp3bcf1802012-09-09 11:17:00 -07001230 private void showConversation(Conversation conv) {
1231 notifyConversationLoaded(conv);
1232 dismissLoadingStatus();
1233 }
1234
1235 @SuppressWarnings("unused")
1236 public void onContentReady() {
1237 final Conversation conv = mConversation;
1238 try {
1239 mHandler.post(new Runnable() {
1240 @Override
1241 public void run() {
1242 LogUtils.d(LOG_TAG, "ANIMATION STARTED, ready to draw. t=%s",
1243 SystemClock.uptimeMillis());
1244 showConversation(conv);
1245 }
1246 });
1247 } catch (Throwable t) {
1248 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onContentReady");
1249 // Still try to show the conversation.
1250 showConversation(conv);
1251 }
1252 }
Andy Huangf70fc402012-02-17 15:37:42 -08001253 }
1254
Andy Huang47aa9c92012-07-31 15:37:21 -07001255 private class NewMessagesInfo {
1256 int count;
1257 String senderAddress;
1258
1259 /**
1260 * Return the display text for the new message notification overlay. It will be formatted
1261 * appropriately for a single new message vs. multiple new messages.
1262 *
1263 * @return display text
1264 */
1265 public String getNotificationText() {
1266 final Object param;
1267 if (count > 1) {
1268 param = count;
1269 } else {
Andy Huang16174812012-08-16 16:40:35 -07001270 final Address addr = getAddress(senderAddress);
Andy Huang47aa9c92012-07-31 15:37:21 -07001271 param = TextUtils.isEmpty(addr.getName()) ? addr.getAddress() : addr.getName();
1272 }
1273 return getResources().getQuantityString(R.plurals.new_incoming_messages, count, param);
1274 }
1275 }
1276
Andy Huangb8331b42012-07-16 19:08:53 -07001277 private class MessageLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
1278
1279 @Override
1280 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
Andy Huangcd5c5ee2012-08-12 19:03:51 -07001281 return new MessageLoader(mContext, mConversation, ConversationViewFragment.this);
Andy Huangb8331b42012-07-16 19:08:53 -07001282 }
1283
1284 @Override
1285 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
1286 MessageCursor messageCursor = (MessageCursor) data;
1287
1288 // ignore truly duplicate results
1289 // this can happen when restoring after rotation
1290 if (mCursor == messageCursor) {
1291 return;
1292 }
1293
Andy Huang47aa9c92012-07-31 15:37:21 -07001294 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
1295 LogUtils.d(LOG_TAG, "LOADED CONVERSATION= %s", messageCursor.getDebugDump());
1296 }
1297
Andy Huang1ee96b22012-08-24 20:19:53 -07001298 // TODO: handle ERROR status
1299
1300 // When the last cursor had message(s), and the new version has no messages,
1301 // we need to exit conversation view.
1302 if (messageCursor.getCount() == 0 && mCursor != null) {
1303
1304 if (mUserVisible) {
1305 // need to exit this view- conversation may have been deleted, or for
1306 // whatever reason is now invalid (e.g. discard single draft)
1307 //
1308 // N.B. this may involve a fragment transaction, which FragmentManager will
1309 // refuse to execute directly within onLoadFinished. Make sure the controller
1310 // knows.
1311 LogUtils.i(LOG_TAG, "CVF: visible conv has no messages, exiting conv mode");
1312 mActivity.getListHandler().onConversationSelected(null,
1313 true /* inLoaderCallbacks */);
1314 } else {
1315 // we expect that the pager adapter will remove this conversation fragment
1316 // on its own due to a separate conversation cursor update
1317 // (we might get here if the message list update fires first. nothing to do
1318 // because we expect to be torn down soon.)
1319 LogUtils.i(LOG_TAG, "CVF: offscreen conv has no messages, ignoring update"
1320 + " in anticipation of conv cursor update. c=%s", mConversation.uri);
1321 }
1322
Andy Huangb8331b42012-07-16 19:08:53 -07001323 return;
1324 }
1325
Andy Huang1ee96b22012-08-24 20:19:53 -07001326 // ignore cursors that are still loading results
1327 if (!messageCursor.isLoaded()) {
Andy Huang47aa9c92012-07-31 15:37:21 -07001328 return;
1329 }
1330
Andy Huangcd5c5ee2012-08-12 19:03:51 -07001331 /*
1332 * what kind of changes affect the MessageCursor?
1333 * 1. new message(s)
1334 * 2. read/unread state change
1335 * 3. deleted message, either regular or draft
1336 * 4. updated message, either from self or from others, updated in content or state
1337 * or sender
1338 * 5. star/unstar of message (technically similar to #1)
1339 * 6. other label change
1340 *
1341 * Use MessageCursor.hashCode() to sort out interesting vs. no-op cursor updates.
1342 */
Andy Huang47aa9c92012-07-31 15:37:21 -07001343
1344 if (mCursor == null) {
Andy Huangcd5c5ee2012-08-12 19:03:51 -07001345 LogUtils.i(LOG_TAG, "CONV RENDER: existing cursor is null, rendering from scratch");
Andy Huang47aa9c92012-07-31 15:37:21 -07001346 } else {
Andy Huangcd5c5ee2012-08-12 19:03:51 -07001347 final NewMessagesInfo info = getNewIncomingMessagesInfo(messageCursor);
1348
1349 if (info.count > 0 || messageCursor.hashCode() == mCursor.hashCode()) {
1350
1351 if (info.count > 0) {
1352 // don't immediately render new incoming messages from other senders
1353 // (to avoid a new message from losing the user's focus)
1354 LogUtils.i(LOG_TAG, "CONV RENDER: conversation updated"
1355 + ", holding cursor for new incoming message");
1356 showNewMessageNotification(info);
1357 } else {
1358 LogUtils.i(LOG_TAG, "CONV RENDER: uninteresting update"
1359 + ", ignoring this conversation update");
1360 }
1361
1362 // update mCursor reference because the old one is about to be closed by
1363 // CursorLoader
1364 mCursor = messageCursor;
1365 return;
1366 }
1367
1368 // cursors are different, and not due to an incoming message. fall through and
1369 // render.
1370 LogUtils.i(LOG_TAG, "CONV RENDER: conversation updated"
1371 + ", but not due to incoming message. rendering.");
Andy Huang47aa9c92012-07-31 15:37:21 -07001372 }
Andy Huangcd5c5ee2012-08-12 19:03:51 -07001373
Andy Huang47aa9c92012-07-31 15:37:21 -07001374 renderConversation(messageCursor);
1375
Andy Huangb8331b42012-07-16 19:08:53 -07001376 // TODO: if this is not user-visible, delay render until user-visible fragment is done.
1377 // This is needed in addition to the showConversation() delay to speed up rotation and
1378 // restoration.
Andy Huangb8331b42012-07-16 19:08:53 -07001379 }
1380
1381 @Override
1382 public void onLoaderReset(Loader<Cursor> loader) {
1383 mCursor = null;
Andy Huangb8331b42012-07-16 19:08:53 -07001384 }
1385
Andy Huang47aa9c92012-07-31 15:37:21 -07001386 private NewMessagesInfo getNewIncomingMessagesInfo(MessageCursor newCursor) {
1387 final NewMessagesInfo info = new NewMessagesInfo();
1388
1389 int pos = -1;
1390 while (newCursor.moveToPosition(++pos)) {
1391 final Message m = newCursor.getMessage();
1392 if (!mViewState.contains(m)) {
1393 LogUtils.i(LOG_TAG, "conversation diff: found new msg: %s", m.uri);
Andy Huang16174812012-08-16 16:40:35 -07001394
1395 final Address from = getAddress(m.from);
1396 // distinguish ours from theirs
1397 // new messages from the account owner should not trigger a notification
1398 if (mAccount.ownsFromAddress(from.getAddress())) {
1399 LogUtils.i(LOG_TAG, "found message from self: %s", m.uri);
1400 continue;
1401 }
1402
Andy Huang47aa9c92012-07-31 15:37:21 -07001403 info.count++;
1404 info.senderAddress = m.from;
1405 }
1406 }
1407 return info;
1408 }
1409
Andy Huangb8331b42012-07-16 19:08:53 -07001410 }
1411
1412 /**
1413 * Inner class to to asynchronously load contact data for all senders in the conversation,
1414 * and notify observers when the data is ready.
1415 *
1416 */
1417 private class ContactLoaderCallbacks implements ContactInfoSource,
1418 LoaderManager.LoaderCallbacks<ImmutableMap<String, ContactInfo>> {
1419
1420 private Set<String> mSenders;
1421 private ImmutableMap<String, ContactInfo> mContactInfoMap;
1422 private DataSetObservable mObservable = new DataSetObservable();
1423
1424 public void setSenders(Set<String> emailAddresses) {
1425 mSenders = emailAddresses;
1426 }
1427
1428 @Override
1429 public Loader<ImmutableMap<String, ContactInfo>> onCreateLoader(int id, Bundle args) {
1430 return new SenderInfoLoader(mContext, mSenders);
1431 }
1432
1433 @Override
1434 public void onLoadFinished(Loader<ImmutableMap<String, ContactInfo>> loader,
1435 ImmutableMap<String, ContactInfo> data) {
1436 mContactInfoMap = data;
1437 mObservable.notifyChanged();
1438 }
1439
1440 @Override
1441 public void onLoaderReset(Loader<ImmutableMap<String, ContactInfo>> loader) {
1442 }
1443
1444 @Override
1445 public ContactInfo getContactInfo(String email) {
1446 if (mContactInfoMap == null) {
1447 return null;
1448 }
1449 return mContactInfoMap.get(email);
1450 }
1451
1452 @Override
1453 public void registerObserver(DataSetObserver observer) {
1454 mObservable.registerObserver(observer);
1455 }
1456
1457 @Override
1458 public void unregisterObserver(DataSetObserver observer) {
1459 mObservable.unregisterObserver(observer);
1460 }
1461
1462 }
1463
Andy Huangc11011d2012-07-24 18:50:34 -07001464 private class MarkReadObserver extends DataSetObserver {
Andy Huangc11011d2012-07-24 18:50:34 -07001465 private final ConversationUpdater mListController;
1466
1467 private MarkReadObserver(ConversationUpdater listController) {
1468 mListController = listController;
1469 }
1470
1471 @Override
1472 public void onChanged() {
1473 if (mListController.getConversationListCursor() == null) {
1474 // nothing yet, keep watching
1475 return;
1476 }
1477 // done loading, safe to mark read now
1478 mListController.unregisterConversationListObserver(this);
1479 mMarkReadObserver = null;
1480 LogUtils.i(LOG_TAG, "running deferred conv mark read on open, id=%d", mConversation.id);
Andy Huangcd5c5ee2012-08-12 19:03:51 -07001481 markReadOnSeen(mListController);
Andy Huangc11011d2012-07-24 18:50:34 -07001482 }
1483 }
1484
Paul Westbrookcebcc642012-08-08 10:06:04 -07001485 private class SetCookieTask extends AsyncTask<Void, Void, Void> {
1486 final String mUri;
1487 final String mCookie;
1488
1489 SetCookieTask(String uri, String cookie) {
1490 mUri = uri;
1491 mCookie = cookie;
1492 }
1493
1494 @Override
1495 public Void doInBackground(Void... args) {
1496 final CookieSyncManager csm =
1497 CookieSyncManager.createInstance(mContext);
1498 CookieManager.getInstance().setCookie(mUri, mCookie);
1499 csm.sync();
1500 return null;
1501 }
1502 }
mindyp36280f32012-09-09 16:11:23 -07001503
1504 public void onConversationUpdated(Conversation conv) {
1505 final ConversationViewHeader headerView = (ConversationViewHeader) mConversationContainer
1506 .findViewById(R.id.conversation_header);
1507 mConversation = conv;
1508 headerView.onConversationUpdated(conv);
1509 }
Mindy Pereira9b875682012-02-15 18:10:54 -08001510}