blob: 4e97e9ba04a9759efcc68a2ac3776074270f9e9a [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
mindypf4fce122012-09-14 15:55:33 -070020
Mindy Pereira9b875682012-02-15 18:10:54 -080021import android.content.Context;
Mindy Pereira8e915722012-02-16 14:42:56 -080022import android.content.Loader;
mindypad0c30d2012-09-25 12:09:13 -070023import android.content.res.Resources;
Mindy Pereira9b875682012-02-15 18:10:54 -080024import android.database.Cursor;
Paul Westbrookcebcc642012-08-08 10:06:04 -070025import android.os.AsyncTask;
Mindy Pereira9b875682012-02-15 18:10:54 -080026import android.os.Bundle;
mindyp3bcf1802012-09-09 11:17:00 -070027import android.os.SystemClock;
Andy Huang47aa9c92012-07-31 15:37:21 -070028import android.text.TextUtils;
Mindy Pereira9b875682012-02-15 18:10:54 -080029import android.view.LayoutInflater;
30import android.view.View;
31import android.view.ViewGroup;
Andy Huangf70fc402012-02-17 15:37:42 -080032import android.webkit.ConsoleMessage;
Paul Westbrookcebcc642012-08-08 10:06:04 -070033import android.webkit.CookieManager;
34import android.webkit.CookieSyncManager;
Mindy Pereira974c9662012-09-14 10:02:08 -070035import android.webkit.JavascriptInterface;
Andy Huangf70fc402012-02-17 15:37:42 -080036import android.webkit.WebChromeClient;
37import android.webkit.WebSettings;
Andy Huang17a9cde2012-03-09 18:03:16 -080038import android.webkit.WebView;
39import android.webkit.WebViewClient;
Andy Huang47aa9c92012-07-31 15:37:21 -070040import android.widget.TextView;
Mindy Pereira9b875682012-02-15 18:10:54 -080041
Andy Huang59e0b182012-08-14 14:32:23 -070042import com.android.mail.FormattedDateBuilder;
Mindy Pereira9b875682012-02-15 18:10:54 -080043import com.android.mail.R;
Andy Huang5ff63742012-03-16 20:30:23 -070044import com.android.mail.browse.ConversationContainer;
Andy Huang46dfba62012-04-19 01:47:32 -070045import com.android.mail.browse.ConversationOverlayItem;
Andy Huang7bdc3752012-03-25 17:18:19 -070046import com.android.mail.browse.ConversationViewAdapter;
Mark Wei56d83852012-09-19 14:28:50 -070047import com.android.mail.browse.ScrollIndicatorsView;
Andy Huang28b7aee2012-08-20 20:27:32 -070048import com.android.mail.browse.ConversationViewAdapter.ConversationAccountController;
Andy Huang46dfba62012-04-19 01:47:32 -070049import com.android.mail.browse.ConversationViewAdapter.MessageFooterItem;
Andy Huang7bdc3752012-03-25 17:18:19 -070050import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem;
Andy Huang46dfba62012-04-19 01:47:32 -070051import com.android.mail.browse.ConversationViewAdapter.SuperCollapsedBlockItem;
Andy Huang5ff63742012-03-16 20:30:23 -070052import com.android.mail.browse.ConversationViewHeader;
53import com.android.mail.browse.ConversationWebView;
mindypdde3f9f2012-09-10 17:35:35 -070054import com.android.mail.browse.ConversationWebView.ContentSizeChangeListener;
Andy Huang7bdc3752012-03-25 17:18:19 -070055import com.android.mail.browse.MessageCursor;
Andy Huangcd5c5ee2012-08-12 19:03:51 -070056import com.android.mail.browse.MessageCursor.ConversationController;
Andy Huang28b7aee2012-08-20 20:27:32 -070057import com.android.mail.browse.MessageCursor.ConversationMessage;
Andy Huang59e0b182012-08-14 14:32:23 -070058import com.android.mail.browse.MessageHeaderView;
Andy Huang3233bff2012-03-20 19:38:45 -070059import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks;
Andy Huang46dfba62012-04-19 01:47:32 -070060import com.android.mail.browse.SuperCollapsedBlock;
Andy Huang0b7ed6f2012-07-25 19:23:26 -070061import com.android.mail.browse.WebViewContextMenu;
Mindy Pereira9b875682012-02-15 18:10:54 -080062import com.android.mail.providers.Account;
Andy Huang65fe28f2012-04-06 18:08:53 -070063import com.android.mail.providers.Address;
Mindy Pereira9b875682012-02-15 18:10:54 -080064import com.android.mail.providers.Conversation;
Andy Huangf70fc402012-02-17 15:37:42 -080065import com.android.mail.providers.Message;
Andy Huangcd5c5ee2012-08-12 19:03:51 -070066import com.android.mail.ui.ConversationViewState.ExpansionState;
Paul Westbrookb334c902012-06-25 11:42:46 -070067import com.android.mail.utils.LogTag;
Mindy Pereira9b875682012-02-15 18:10:54 -080068import com.android.mail.utils.LogUtils;
Andy Huang2e9acfe2012-03-15 22:39:36 -070069import com.android.mail.utils.Utils;
Andy Huang46dfba62012-04-19 01:47:32 -070070import com.google.common.collect.Lists;
Andy Huangb8331b42012-07-16 19:08:53 -070071import com.google.common.collect.Sets;
Andy Huang65fe28f2012-04-06 18:08:53 -070072
Andy Huang46dfba62012-04-19 01:47:32 -070073import java.util.List;
Andy Huangb8331b42012-07-16 19:08:53 -070074import java.util.Set;
Mindy Pereira9b875682012-02-15 18:10:54 -080075
Andy Huangf70fc402012-02-17 15:37:42 -080076
Mindy Pereira9b875682012-02-15 18:10:54 -080077/**
78 * The conversation view UI component.
79 */
mindypf4fce122012-09-14 15:55:33 -070080public final class ConversationViewFragment extends AbstractConversationViewFragment implements
Andy Huang46dfba62012-04-19 01:47:32 -070081 MessageHeaderViewCallbacks,
Andy Huangcd5c5ee2012-08-12 19:03:51 -070082 SuperCollapsedBlock.OnClickListener,
Andy Huang28b7aee2012-08-20 20:27:32 -070083 ConversationController,
84 ConversationAccountController {
Mindy Pereira8e915722012-02-16 14:42:56 -080085
Paul Westbrookb334c902012-06-25 11:42:46 -070086 private static final String LOG_TAG = LogTag.getLogTag();
Andy Huang632721e2012-04-11 16:57:26 -070087 public static final String LAYOUT_TAG = "ConvLayout";
Mindy Pereira9b875682012-02-15 18:10:54 -080088
mindyp3bcf1802012-09-09 11:17:00 -070089 /** Do not auto load data when create this {@link ConversationView}. */
90 public static final int NO_AUTO_LOAD = 0;
91 /** Auto load data but do not show any animation. */
92 public static final int AUTO_LOAD_BACKGROUND = 1;
93 /** Auto load data and show animation. */
94 public static final int AUTO_LOAD_VISIBLE = 2;
95
Andy Huangf70fc402012-02-17 15:37:42 -080096 private ConversationContainer mConversationContainer;
Mindy Pereira9b875682012-02-15 18:10:54 -080097
Andy Huangf70fc402012-02-17 15:37:42 -080098 private ConversationWebView mWebView;
Mindy Pereira9b875682012-02-15 18:10:54 -080099
Mark Wei56d83852012-09-19 14:28:50 -0700100 private ScrollIndicatorsView mScrollIndicators;
101
Andy Huang47aa9c92012-07-31 15:37:21 -0700102 private View mNewMessageBar;
103
Andy Huangf70fc402012-02-17 15:37:42 -0800104 private HtmlConversationTemplates mTemplates;
105
Andy Huangf70fc402012-02-17 15:37:42 -0800106 private final MailJsBridge mJsBridge = new MailJsBridge();
107
Andy Huang17a9cde2012-03-09 18:03:16 -0800108 private final WebViewClient mWebViewClient = new ConversationWebViewClient();
109
Andy Huang7bdc3752012-03-25 17:18:19 -0700110 private ConversationViewAdapter mAdapter;
Andy Huang51067132012-03-12 20:08:19 -0700111
112 private boolean mViewsCreated;
113
Andy Huang46dfba62012-04-19 01:47:32 -0700114 /**
115 * Temporary string containing the message bodies of the messages within a super-collapsed
116 * block, for one-time use during block expansion. We cannot easily pass the body HTML
117 * into JS without problematic escaping, so hold onto it momentarily and signal JS to fetch it
118 * using {@link MailJsBridge}.
119 */
120 private String mTempBodiesHtml;
121
Andy Huang632721e2012-04-11 16:57:26 -0700122 private int mMaxAutoLoadMessages;
123
124 private boolean mDeferredConversationLoad;
125
mindyp3bcf1802012-09-09 11:17:00 -0700126 private boolean mEnableContentReadySignal;
Andy Huang28b7aee2012-08-20 20:27:32 -0700127
mindypdde3f9f2012-09-10 17:35:35 -0700128 private ContentSizeChangeListener mWebViewSizeChangeListener;
129
Andy Huang839ada22012-07-20 15:48:40 -0700130 private static final String BUNDLE_VIEW_STATE = "viewstate";
Andy Huangf70fc402012-02-17 15:37:42 -0800131
Andy Huangbd544e32012-05-29 15:56:51 -0700132 private static final boolean DEBUG_DUMP_CONVERSATION_HTML = false;
Andy Huang47aa9c92012-07-31 15:37:21 -0700133 private static final boolean DISABLE_OFFSCREEN_LOADING = false;
mindyp3bcf1802012-09-09 11:17:00 -0700134 protected static final String AUTO_LOAD_KEY = "auto-load";
Andy Huangbd544e32012-05-29 15:56:51 -0700135
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800136 /**
137 * Constructor needs to be public to handle orientation changes and activity lifecycle events.
138 */
Andy Huangf70fc402012-02-17 15:37:42 -0800139 public ConversationViewFragment() {
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800140 super();
Mindy Pereira9b875682012-02-15 18:10:54 -0800141 }
142
143 /**
144 * Creates a new instance of {@link ConversationViewFragment}, initialized
Andy Huang632721e2012-04-11 16:57:26 -0700145 * to display a conversation with other parameters inherited/copied from an existing bundle,
146 * typically one created using {@link #makeBasicArgs}.
147 */
148 public static ConversationViewFragment newInstance(Bundle existingArgs,
149 Conversation conversation) {
150 ConversationViewFragment f = new ConversationViewFragment();
151 Bundle args = new Bundle(existingArgs);
152 args.putParcelable(ARG_CONVERSATION, conversation);
153 f.setArguments(args);
154 return f;
155 }
156
mindypf4fce122012-09-14 15:55:33 -0700157 @Override
158 public void onAccountChanged() {
159 // settings may have been updated; refresh views that are known to
160 // depend on settings
161 mConversationContainer.getSnapHeader().onAccountChanged();
162 mAdapter.notifyDataSetChanged();
Andy Huang632721e2012-04-11 16:57:26 -0700163 }
164
Mindy Pereira9b875682012-02-15 18:10:54 -0800165 @Override
166 public void onActivityCreated(Bundle savedInstanceState) {
Andy Huang632721e2012-04-11 16:57:26 -0700167 LogUtils.d(LOG_TAG, "IN CVF.onActivityCreated, this=%s subj=%s", this,
168 mConversation.subject);
Mindy Pereira9b875682012-02-15 18:10:54 -0800169 super.onActivityCreated(savedInstanceState);
mindypf4fce122012-09-14 15:55:33 -0700170 Context context = getContext();
171 mTemplates = new HtmlConversationTemplates(context);
Andy Huang59e0b182012-08-14 14:32:23 -0700172
mindypf4fce122012-09-14 15:55:33 -0700173 final FormattedDateBuilder dateBuilder = new FormattedDateBuilder(context);
Andy Huang59e0b182012-08-14 14:32:23 -0700174
Paul Westbrook8081df42012-09-10 15:43:36 -0700175 mAdapter = new ConversationViewAdapter(mActivity, this,
mindypf4fce122012-09-14 15:55:33 -0700176 getLoaderManager(), this, getContactInfoSource(), this,
Paul Westbrook8081df42012-09-10 15:43:36 -0700177 this, mAddressCache, dateBuilder);
Andy Huang51067132012-03-12 20:08:19 -0700178 mConversationContainer.setOverlayAdapter(mAdapter);
179
Andy Huang59e0b182012-08-14 14:32:23 -0700180 // set up snap header (the adapter usually does this with the other ones)
181 final MessageHeaderView snapHeader = mConversationContainer.getSnapHeader();
Andy Huang28b7aee2012-08-20 20:27:32 -0700182 snapHeader.initialize(dateBuilder, this, mAddressCache);
Andy Huang59e0b182012-08-14 14:32:23 -0700183 snapHeader.setCallbacks(this);
mindypf4fce122012-09-14 15:55:33 -0700184 snapHeader.setContactInfoSource(getContactInfoSource());
Andy Huang59e0b182012-08-14 14:32:23 -0700185
Andy Huang632721e2012-04-11 16:57:26 -0700186 mMaxAutoLoadMessages = getResources().getInteger(R.integer.max_auto_load_messages);
187
mindypf4fce122012-09-14 15:55:33 -0700188 mWebView.setOnCreateContextMenuListener(new WebViewContextMenu(getActivity()));
Andy Huang0b7ed6f2012-07-25 19:23:26 -0700189
Mindy Pereira9b875682012-02-15 18:10:54 -0800190 showConversation();
Paul Westbrookcebcc642012-08-08 10:06:04 -0700191
192 if (mConversation.conversationBaseUri != null &&
193 !TextUtils.isEmpty(mConversation.conversationCookie)) {
194 // Set the cookie for this base url
195 new SetCookieTask(mConversation.conversationBaseUri.toString(),
196 mConversation.conversationCookie).execute();
197 }
Mindy Pereira9b875682012-02-15 18:10:54 -0800198 }
199
Mindy Pereira9b875682012-02-15 18:10:54 -0800200 @Override
201 public View onCreateView(LayoutInflater inflater,
202 ViewGroup container, Bundle savedInstanceState) {
Andy Huang839ada22012-07-20 15:48:40 -0700203 if (savedInstanceState != null) {
204 mViewState = savedInstanceState.getParcelable(BUNDLE_VIEW_STATE);
205 } else {
Paul Westbrook4d8cad52012-09-21 14:13:49 -0700206 mViewState = getNewViewState();
Andy Huang839ada22012-07-20 15:48:40 -0700207 }
208
Andy Huang632721e2012-04-11 16:57:26 -0700209 View rootView = inflater.inflate(R.layout.conversation_view, container, false);
Andy Huangf70fc402012-02-17 15:37:42 -0800210 mConversationContainer = (ConversationContainer) rootView
211 .findViewById(R.id.conversation_container);
Andy Huang47aa9c92012-07-31 15:37:21 -0700212
213 mNewMessageBar = mConversationContainer.findViewById(R.id.new_message_notification_bar);
214 mNewMessageBar.setOnClickListener(new View.OnClickListener() {
215 @Override
216 public void onClick(View v) {
217 onNewMessageBarClick();
218 }
219 });
220
mindypff282d02012-09-17 10:33:02 -0700221 instantiateProgressIndicators(rootView);
mindyp3bcf1802012-09-09 11:17:00 -0700222
Andy Huang5ff63742012-03-16 20:30:23 -0700223 mWebView = (ConversationWebView) mConversationContainer.findViewById(R.id.webview);
Andy Huangf70fc402012-02-17 15:37:42 -0800224
Andy Huangf70fc402012-02-17 15:37:42 -0800225 mWebView.addJavascriptInterface(mJsBridge, "mail");
mindyp3bcf1802012-09-09 11:17:00 -0700226 // On JB or newer, we use the 'webkitAnimationStart' DOM event to signal load complete
227 // Below JB, try to speed up initial render by having the webview do supplemental draws to
228 // custom a software canvas.
mindypb941fdb2012-09-11 08:28:23 -0700229 // TODO(mindyp):
230 //PAGE READINESS SIGNAL FOR JELLYBEAN AND NEWER
231 // Notify the app on 'webkitAnimationStart' of a simple dummy element with a simple no-op
232 // animation that immediately runs on page load. The app uses this as a signal that the
233 // content is loaded and ready to draw, since WebView delays firing this event until the
234 // layers are composited and everything is ready to draw.
235 // This signal does not seem to be reliable, so just use the old method for now.
mindyp32d911f2012-09-24 15:14:22 -0700236 mEnableContentReadySignal = Utils.isRunningJellybeanOrLater();
mindypafc9b362012-09-25 09:20:47 -0700237 mWebView.setUseSoftwareLayer(!mEnableContentReadySignal);
Andy Huang17a9cde2012-03-09 18:03:16 -0800238 mWebView.setWebViewClient(mWebViewClient);
Andy Huangf70fc402012-02-17 15:37:42 -0800239 mWebView.setWebChromeClient(new WebChromeClient() {
240 @Override
241 public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
242 LogUtils.i(LOG_TAG, "JS: %s (%s:%d)", consoleMessage.message(),
243 consoleMessage.sourceId(), consoleMessage.lineNumber());
244 return true;
245 }
246 });
247
Andy Huang3233bff2012-03-20 19:38:45 -0700248 final WebSettings settings = mWebView.getSettings();
Andy Huangf70fc402012-02-17 15:37:42 -0800249
Mark Wei56d83852012-09-19 14:28:50 -0700250 mScrollIndicators = (ScrollIndicatorsView) rootView.findViewById(R.id.scroll_indicators);
251 mScrollIndicators.setSourceView(mWebView);
252
Andy Huangf70fc402012-02-17 15:37:42 -0800253 settings.setJavaScriptEnabled(true);
254 settings.setUseWideViewPort(true);
Andy Huang23014702012-07-09 12:50:36 -0700255 settings.setLoadWithOverviewMode(true);
Andy Huangf70fc402012-02-17 15:37:42 -0800256
257 settings.setSupportZoom(true);
258 settings.setBuiltInZoomControls(true);
259 settings.setDisplayZoomControls(false);
Mindy Pereira9b875682012-02-15 18:10:54 -0800260
Andy Huangc319b552012-04-25 19:53:50 -0700261 final float fontScale = getResources().getConfiguration().fontScale;
Andy Huangba283732012-06-25 19:14:10 -0700262 final int desiredFontSizePx = getResources()
263 .getInteger(R.integer.conversation_desired_font_size_px);
264 final int unstyledFontSizePx = getResources()
265 .getInteger(R.integer.conversation_unstyled_font_size_px);
Andy Huangc319b552012-04-25 19:53:50 -0700266
Andy Huangba283732012-06-25 19:14:10 -0700267 int textZoom = settings.getTextZoom();
268 // apply a correction to the default body text style to get regular text to the size we want
269 textZoom = textZoom * desiredFontSizePx / unstyledFontSizePx;
270 // then apply any system font scaling
Andy Huangc319b552012-04-25 19:53:50 -0700271 textZoom = (int) (textZoom * fontScale);
272 settings.setTextZoom(textZoom);
273
Andy Huang51067132012-03-12 20:08:19 -0700274 mViewsCreated = true;
275
Mindy Pereira9b875682012-02-15 18:10:54 -0800276 return rootView;
277 }
278
279 @Override
Andy Huanga6e965e2012-08-21 13:44:34 -0700280 public void onResume() {
281 super.onResume();
282
283 // Hacky workaround for http://b/6946182
284 Utils.fixSubTreeLayoutIfOrphaned(getView(), "ConversationViewFragment");
285 }
286
287 @Override
Andy Huang839ada22012-07-20 15:48:40 -0700288 public void onSaveInstanceState(Bundle outState) {
289 if (mViewState != null) {
290 outState.putParcelable(BUNDLE_VIEW_STATE, mViewState);
291 }
292 }
293
294 @Override
Mindy Pereira9b875682012-02-15 18:10:54 -0800295 public void onDestroyView() {
Mindy Pereira9b875682012-02-15 18:10:54 -0800296 super.onDestroyView();
Andy Huang46dfba62012-04-19 01:47:32 -0700297 mConversationContainer.setOverlayAdapter(null);
298 mAdapter = null;
Andy Huang51067132012-03-12 20:08:19 -0700299 mViewsCreated = false;
Mindy Pereira9b875682012-02-15 18:10:54 -0800300 }
301
Andy Huang5ff63742012-03-16 20:30:23 -0700302 @Override
mindypf4fce122012-09-14 15:55:33 -0700303 protected void markUnread() {
Andy Huang839ada22012-07-20 15:48:40 -0700304 // Ignore unsafe calls made after a fragment is detached from an activity
305 final ControllableActivity activity = (ControllableActivity) getActivity();
306 if (activity == null) {
307 LogUtils.w(LOG_TAG, "ignoring markUnread for conv=%s", mConversation.id);
308 return;
309 }
310
Andy Huang28e31e22012-07-26 16:33:15 -0700311 if (mViewState == null) {
312 LogUtils.i(LOG_TAG, "ignoring markUnread for conv with no view state (%d)",
313 mConversation.id);
314 return;
315 }
Andy Huang839ada22012-07-20 15:48:40 -0700316 activity.getConversationUpdater().markConversationMessagesUnread(mConversation,
Vikram Aggarwal4a878b62012-07-31 15:09:25 -0700317 mViewState.getUnreadMessageUris(), mViewState.getConversationInfo());
Andy Huang839ada22012-07-20 15:48:40 -0700318 }
319
mindypf4fce122012-09-14 15:55:33 -0700320 @Override
321 public void onUserVisibleHintChanged() {
322 if (mUserVisible && mViewsCreated) {
323 Cursor cursor = getMessageCursor();
324 if (cursor == null && mDeferredConversationLoad) {
325 // load
326 LogUtils.v(LOG_TAG, "Fragment is now user-visible, showing conversation: %s",
327 mConversation.uri);
328 showConversation();
329 mDeferredConversationLoad = false;
330 } else {
331 onConversationSeen();
Andy Huang632721e2012-04-11 16:57:26 -0700332 }
mindyp32d911f2012-09-24 15:14:22 -0700333 } else if (!mUserVisible) {
334 dismissLoadingStatus();
Andy Huang632721e2012-04-11 16:57:26 -0700335 }
336 }
337
Mindy Pereira9b875682012-02-15 18:10:54 -0800338 private void showConversation() {
mindyp3bcf1802012-09-09 11:17:00 -0700339 final boolean disableOffscreenLoading = DISABLE_OFFSCREEN_LOADING
340 || (mConversation.isRemote
341 || mConversation.getNumMessages() > mMaxAutoLoadMessages);
Andy Huang47aa9c92012-07-31 15:37:21 -0700342 if (!mUserVisible && disableOffscreenLoading) {
Andy Huang632721e2012-04-11 16:57:26 -0700343 LogUtils.v(LOG_TAG, "Fragment not user-visible, not showing conversation: %s",
344 mConversation.uri);
345 mDeferredConversationLoad = true;
346 return;
347 }
348 LogUtils.v(LOG_TAG,
349 "Fragment is short or user-visible, immediately rendering conversation: %s",
350 mConversation.uri);
mindyp3bcf1802012-09-09 11:17:00 -0700351 mWebView.setVisibility(View.VISIBLE);
mindypf4fce122012-09-14 15:55:33 -0700352 getLoaderManager().initLoader(MESSAGE_LOADER, Bundle.EMPTY, getMessageLoaderCallbacks());
Andy Huang5460ce42012-08-16 19:38:27 -0700353 if (mUserVisible) {
354 final SubjectDisplayChanger sdc = mActivity.getSubjectDisplayChanger();
355 if (sdc != null) {
356 sdc.setSubject(mConversation.subject);
357 }
358 }
mindyp3bcf1802012-09-09 11:17:00 -0700359 // TODO(mindyp): don't show loading status for a previously rendered
360 // conversation. Ielieve this is better done by making sure don't show loading status
361 // until XX ms have passed without loading completed.
362 showLoadingStatus();
Mindy Pereira8e915722012-02-16 14:42:56 -0800363 }
364
Andy Huang51067132012-03-12 20:08:19 -0700365 private void renderConversation(MessageCursor messageCursor) {
mindyp3bcf1802012-09-09 11:17:00 -0700366 final String convHtml = renderMessageBodies(messageCursor, mEnableContentReadySignal);
Andy Huangbd544e32012-05-29 15:56:51 -0700367
368 if (DEBUG_DUMP_CONVERSATION_HTML) {
369 java.io.FileWriter fw = null;
370 try {
371 fw = new java.io.FileWriter("/sdcard/conv" + mConversation.id
372 + ".html");
373 fw.write(convHtml);
374 } catch (java.io.IOException e) {
375 e.printStackTrace();
376 } finally {
377 if (fw != null) {
378 try {
379 fw.close();
380 } catch (java.io.IOException e) {
381 e.printStackTrace();
382 }
383 }
384 }
385 }
386
387 mWebView.loadDataWithBaseURL(mBaseUri, convHtml, "text/html", "utf-8", null);
Andy Huang51067132012-03-12 20:08:19 -0700388 }
389
Andy Huang7bdc3752012-03-25 17:18:19 -0700390 /**
391 * Populate the adapter with overlay views (message headers, super-collapsed blocks, a
392 * conversation header), and return an HTML document with spacer divs inserted for all overlays.
393 *
394 */
mindyp3bcf1802012-09-09 11:17:00 -0700395 private String renderMessageBodies(MessageCursor messageCursor,
396 boolean enableContentReadySignal) {
Andy Huangf70fc402012-02-17 15:37:42 -0800397 int pos = -1;
Andy Huang632721e2012-04-11 16:57:26 -0700398
Andy Huang1ee96b22012-08-24 20:19:53 -0700399 LogUtils.d(LOG_TAG, "IN renderMessageBodies, fragment=%s", this);
Andy Huang7bdc3752012-03-25 17:18:19 -0700400 boolean allowNetworkImages = false;
401
Andy Huangc7543572012-04-03 15:34:29 -0700402 // TODO: re-use any existing adapter item state (expanded, details expanded, show pics)
Andy Huang28b7aee2012-08-20 20:27:32 -0700403
Andy Huang7bdc3752012-03-25 17:18:19 -0700404 // Walk through the cursor and build up an overlay adapter as you go.
405 // Each overlay has an entry in the adapter for easy scroll handling in the container.
406 // Items are not necessarily 1:1 in cursor and adapter because of super-collapsed blocks.
407 // When adding adapter items, also add their heights to help the container later determine
408 // overlay dimensions.
409
Andy Huangdb620fe2012-08-24 15:45:28 -0700410 // When re-rendering, prevent ConversationContainer from laying out overlays until after
411 // the new spacers are positioned by WebView.
412 mConversationContainer.invalidateSpacerGeometry();
413
Andy Huang7bdc3752012-03-25 17:18:19 -0700414 mAdapter.clear();
415
Andy Huang47aa9c92012-07-31 15:37:21 -0700416 // re-evaluate the message parts of the view state, since the messages may have changed
417 // since the previous render
418 final ConversationViewState prevState = mViewState;
419 mViewState = new ConversationViewState(prevState);
420
Andy Huang5ff63742012-03-16 20:30:23 -0700421 // N.B. the units of height for spacers are actually dp and not px because WebView assumes
Andy Huang2e9acfe2012-03-15 22:39:36 -0700422 // a pixel is an mdpi pixel, unless you set device-dpi.
Andy Huang5ff63742012-03-16 20:30:23 -0700423
Andy Huang7bdc3752012-03-25 17:18:19 -0700424 // add a single conversation header item
425 final int convHeaderPos = mAdapter.addConversationHeader(mConversation);
Andy Huang23014702012-07-09 12:50:36 -0700426 final int convHeaderPx = measureOverlayHeight(convHeaderPos);
Andy Huang5ff63742012-03-16 20:30:23 -0700427
Andy Huang256b35c2012-08-22 15:19:13 -0700428 final int sideMarginPx = getResources().getDimensionPixelOffset(
429 R.dimen.conversation_view_margin_side) + getResources().getDimensionPixelOffset(
430 R.dimen.conversation_message_content_margin_side);
431
432 mTemplates.startConversation(mWebView.screenPxToWebPx(sideMarginPx),
433 mWebView.screenPxToWebPx(convHeaderPx));
Andy Huang3233bff2012-03-20 19:38:45 -0700434
Andy Huang46dfba62012-04-19 01:47:32 -0700435 int collapsedStart = -1;
Andy Huang839ada22012-07-20 15:48:40 -0700436 ConversationMessage prevCollapsedMsg = null;
Andy Huang46dfba62012-04-19 01:47:32 -0700437 boolean prevSafeForImages = false;
438
Andy Huangf70fc402012-02-17 15:37:42 -0800439 while (messageCursor.moveToPosition(++pos)) {
Andy Huang839ada22012-07-20 15:48:40 -0700440 final ConversationMessage msg = messageCursor.getMessage();
Andy Huang46dfba62012-04-19 01:47:32 -0700441
Andy Huang3233bff2012-03-20 19:38:45 -0700442 // TODO: save/restore 'show pics' state
443 final boolean safeForImages = msg.alwaysShowImages /* || savedStateSaysSafe */;
444 allowNetworkImages |= safeForImages;
Andy Huang24055282012-03-27 17:37:06 -0700445
Paul Westbrook08098ec2012-08-12 15:30:28 -0700446 final Integer savedExpanded = prevState.getExpansionState(msg);
447 final int expandedState;
Andy Huang839ada22012-07-20 15:48:40 -0700448 if (savedExpanded != null) {
Andy Huang1ee96b22012-08-24 20:19:53 -0700449 if (ExpansionState.isSuperCollapsed(savedExpanded) && messageCursor.isLast()) {
450 // override saved state when this is now the new last message
451 // this happens to the second-to-last message when you discard a draft
452 expandedState = ExpansionState.EXPANDED;
453 } else {
454 expandedState = savedExpanded;
455 }
Andy Huang839ada22012-07-20 15:48:40 -0700456 } else {
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700457 // new messages that are not expanded default to being eligible for super-collapse
Paul Westbrook08098ec2012-08-12 15:30:28 -0700458 expandedState = (!msg.read || msg.starred || messageCursor.isLast()) ?
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700459 ExpansionState.EXPANDED : ExpansionState.SUPER_COLLAPSED;
Andy Huang839ada22012-07-20 15:48:40 -0700460 }
Paul Westbrook08098ec2012-08-12 15:30:28 -0700461 mViewState.setExpansionState(msg, expandedState);
Andy Huangc7543572012-04-03 15:34:29 -0700462
Andy Huang839ada22012-07-20 15:48:40 -0700463 // save off "read" state from the cursor
464 // later, the view may not match the cursor (e.g. conversation marked read on open)
Andy Huang423bea22012-08-21 12:00:49 -0700465 // however, if a previous state indicated this message was unread, trust that instead
466 // so "mark unread" marks all originally unread messages
467 mViewState.setReadState(msg, msg.read && !prevState.isUnread(msg));
Andy Huang839ada22012-07-20 15:48:40 -0700468
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700469 // We only want to consider this for inclusion in the super collapsed block if
470 // 1) The we don't have previous state about this message (The first time that the
471 // user opens a conversation)
472 // 2) The previously saved state for this message indicates that this message is
473 // in the super collapsed block.
474 if (ExpansionState.isSuperCollapsed(expandedState)) {
475 // contribute to a super-collapsed block that will be emitted just before the
476 // next expanded header
477 if (collapsedStart < 0) {
478 collapsedStart = pos;
Andy Huang46dfba62012-04-19 01:47:32 -0700479 }
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700480 prevCollapsedMsg = msg;
481 prevSafeForImages = safeForImages;
482 continue;
Andy Huang46dfba62012-04-19 01:47:32 -0700483 }
Andy Huang24055282012-03-27 17:37:06 -0700484
Andy Huang46dfba62012-04-19 01:47:32 -0700485 // resolve any deferred decisions on previous collapsed items
486 if (collapsedStart >= 0) {
487 if (pos - collapsedStart == 1) {
488 // special-case for a single collapsed message: no need to super-collapse it
489 renderMessage(prevCollapsedMsg, false /* expanded */,
490 prevSafeForImages);
491 } else {
492 renderSuperCollapsedBlock(collapsedStart, pos - 1);
493 }
494 prevCollapsedMsg = null;
495 collapsedStart = -1;
496 }
Andy Huang7bdc3752012-03-25 17:18:19 -0700497
Paul Westbrook08098ec2012-08-12 15:30:28 -0700498 renderMessage(msg, ExpansionState.isExpanded(expandedState), safeForImages);
Mindy Pereira9b875682012-02-15 18:10:54 -0800499 }
Andy Huang3233bff2012-03-20 19:38:45 -0700500
501 mWebView.getSettings().setBlockNetworkImage(!allowNetworkImages);
502
Paul Westbrookcebcc642012-08-08 10:06:04 -0700503 // If the conversation has specified a base uri, use it here, use mBaseUri
504 final String conversationBaseUri = mConversation.conversationBaseUri != null ?
505 mConversation.conversationBaseUri.toString() : mBaseUri;
506 return mTemplates.endConversation(mBaseUri, conversationBaseUri, 320,
mindyp3bcf1802012-09-09 11:17:00 -0700507 mWebView.getViewportWidth(), enableContentReadySignal);
Mindy Pereira9b875682012-02-15 18:10:54 -0800508 }
Mindy Pereira674afa42012-02-17 14:05:24 -0800509
Andy Huang46dfba62012-04-19 01:47:32 -0700510 private void renderSuperCollapsedBlock(int start, int end) {
511 final int blockPos = mAdapter.addSuperCollapsedBlock(start, end);
Andy Huang23014702012-07-09 12:50:36 -0700512 final int blockPx = measureOverlayHeight(blockPos);
513 mTemplates.appendSuperCollapsedHtml(start, mWebView.screenPxToWebPx(blockPx));
Andy Huang46dfba62012-04-19 01:47:32 -0700514 }
515
Andy Huang839ada22012-07-20 15:48:40 -0700516 private void renderMessage(ConversationMessage msg, boolean expanded,
517 boolean safeForImages) {
Andy Huang46dfba62012-04-19 01:47:32 -0700518 final int headerPos = mAdapter.addMessageHeader(msg, expanded);
519 final MessageHeaderItem headerItem = (MessageHeaderItem) mAdapter.getItem(headerPos);
520
521 final int footerPos = mAdapter.addMessageFooter(headerItem);
522
523 // Measure item header and footer heights to allocate spacers in HTML
524 // But since the views themselves don't exist yet, render each item temporarily into
525 // a host view for measurement.
Andy Huang23014702012-07-09 12:50:36 -0700526 final int headerPx = measureOverlayHeight(headerPos);
527 final int footerPx = measureOverlayHeight(footerPos);
Andy Huang46dfba62012-04-19 01:47:32 -0700528
Andy Huang256b35c2012-08-22 15:19:13 -0700529 mTemplates.appendMessageHtml(msg, expanded, safeForImages,
Andy Huang23014702012-07-09 12:50:36 -0700530 mWebView.screenPxToWebPx(headerPx), mWebView.screenPxToWebPx(footerPx));
Andy Huang46dfba62012-04-19 01:47:32 -0700531 }
532
533 private String renderCollapsedHeaders(MessageCursor cursor,
534 SuperCollapsedBlockItem blockToReplace) {
535 final List<ConversationOverlayItem> replacements = Lists.newArrayList();
536
537 mTemplates.reset();
538
Mark Wei2b24e992012-09-10 16:40:07 -0700539 // In devices with non-integral density multiplier, screen pixels translate to non-integral
540 // web pixels. Keep track of the error that occurs when we cast all heights to int
541 float error = 0f;
Andy Huang46dfba62012-04-19 01:47:32 -0700542 for (int i = blockToReplace.getStart(), end = blockToReplace.getEnd(); i <= end; i++) {
543 cursor.moveToPosition(i);
Andy Huang839ada22012-07-20 15:48:40 -0700544 final ConversationMessage msg = cursor.getMessage();
Andy Huang46dfba62012-04-19 01:47:32 -0700545 final MessageHeaderItem header = mAdapter.newMessageHeaderItem(msg,
546 false /* expanded */);
547 final MessageFooterItem footer = mAdapter.newMessageFooterItem(header);
548
Andy Huang23014702012-07-09 12:50:36 -0700549 final int headerPx = measureOverlayHeight(header);
550 final int footerPx = measureOverlayHeight(footer);
Mark Wei2b24e992012-09-10 16:40:07 -0700551 error += mWebView.screenPxToWebPxError(headerPx)
552 + mWebView.screenPxToWebPxError(footerPx);
553
554 // When the error becomes greater than 1 pixel, make the next header 1 pixel taller
555 int correction = 0;
556 if (error >= 1) {
557 correction = 1;
558 error -= 1;
559 }
Andy Huang46dfba62012-04-19 01:47:32 -0700560
Andy Huang256b35c2012-08-22 15:19:13 -0700561 mTemplates.appendMessageHtml(msg, false /* expanded */, msg.alwaysShowImages,
Mark Wei2b24e992012-09-10 16:40:07 -0700562 mWebView.screenPxToWebPx(headerPx) + correction,
563 mWebView.screenPxToWebPx(footerPx));
Andy Huang46dfba62012-04-19 01:47:32 -0700564 replacements.add(header);
565 replacements.add(footer);
Andy Huang839ada22012-07-20 15:48:40 -0700566
Paul Westbrook08098ec2012-08-12 15:30:28 -0700567 mViewState.setExpansionState(msg, ExpansionState.COLLAPSED);
Andy Huang46dfba62012-04-19 01:47:32 -0700568 }
569
570 mAdapter.replaceSuperCollapsedBlock(blockToReplace, replacements);
571
572 return mTemplates.emit();
573 }
574
575 private int measureOverlayHeight(int position) {
576 return measureOverlayHeight(mAdapter.getItem(position));
577 }
578
Andy Huang7bdc3752012-03-25 17:18:19 -0700579 /**
Andy Huangb8331b42012-07-16 19:08:53 -0700580 * Measure the height of an adapter view by rendering an adapter item into a temporary
Andy Huang46dfba62012-04-19 01:47:32 -0700581 * host view, and asking the view to immediately measure itself. This method will reuse
Andy Huang7bdc3752012-03-25 17:18:19 -0700582 * a previous adapter view from {@link ConversationContainer}'s scrap views if one was generated
583 * earlier.
584 * <p>
Andy Huang46dfba62012-04-19 01:47:32 -0700585 * After measuring the height, this method also saves the height in the
586 * {@link ConversationOverlayItem} for later use in overlay positioning.
Andy Huang7bdc3752012-03-25 17:18:19 -0700587 *
Andy Huang46dfba62012-04-19 01:47:32 -0700588 * @param convItem adapter item with data to render and measure
Andy Huang23014702012-07-09 12:50:36 -0700589 * @return height of the rendered view in screen px
Andy Huang7bdc3752012-03-25 17:18:19 -0700590 */
Andy Huang46dfba62012-04-19 01:47:32 -0700591 private int measureOverlayHeight(ConversationOverlayItem convItem) {
Andy Huang7bdc3752012-03-25 17:18:19 -0700592 final int type = convItem.getType();
593
594 final View convertView = mConversationContainer.getScrapView(type);
Andy Huangb8331b42012-07-16 19:08:53 -0700595 final View hostView = mAdapter.getView(convItem, convertView, mConversationContainer,
596 true /* measureOnly */);
Andy Huang7bdc3752012-03-25 17:18:19 -0700597 if (convertView == null) {
598 mConversationContainer.addScrapView(type, hostView);
599 }
600
Andy Huang9875bb42012-04-04 20:36:21 -0700601 final int heightPx = mConversationContainer.measureOverlay(hostView);
Andy Huang7bdc3752012-03-25 17:18:19 -0700602 convItem.setHeight(heightPx);
Andy Huang9875bb42012-04-04 20:36:21 -0700603 convItem.markMeasurementValid();
Andy Huang7bdc3752012-03-25 17:18:19 -0700604
Andy Huang23014702012-07-09 12:50:36 -0700605 return heightPx;
Andy Huang7bdc3752012-03-25 17:18:19 -0700606 }
607
Andy Huang5ff63742012-03-16 20:30:23 -0700608 @Override
609 public void onConversationViewHeaderHeightChange(int newHeight) {
610 // TODO: propagate the new height to the header's HTML spacer. This can happen when labels
611 // are added/removed
612 }
613
Andy Huang3233bff2012-03-20 19:38:45 -0700614 // END conversation header callbacks
615
616 // START message header callbacks
617 @Override
Andy Huangc7543572012-04-03 15:34:29 -0700618 public void setMessageSpacerHeight(MessageHeaderItem item, int newSpacerHeightPx) {
619 mConversationContainer.invalidateSpacerGeometry();
620
621 // update message HTML spacer height
Andy Huang23014702012-07-09 12:50:36 -0700622 final int h = mWebView.screenPxToWebPx(newSpacerHeightPx);
623 LogUtils.i(LAYOUT_TAG, "setting HTML spacer h=%dwebPx (%dscreenPx)", h,
624 newSpacerHeightPx);
Vikram Aggarwal5349ce12012-09-24 14:12:40 -0700625 mWebView.loadUrl(String.format("javascript:setMessageHeaderSpacerHeight('%s', %s);",
Andy Huang014ea4c2012-09-25 14:50:54 -0700626 mTemplates.getMessageDomId(item.getMessage()), h));
Andy Huang3233bff2012-03-20 19:38:45 -0700627 }
628
629 @Override
Andy Huangc7543572012-04-03 15:34:29 -0700630 public void setMessageExpanded(MessageHeaderItem item, int newSpacerHeightPx) {
631 mConversationContainer.invalidateSpacerGeometry();
632
633 // show/hide the HTML message body and update the spacer height
Andy Huang23014702012-07-09 12:50:36 -0700634 final int h = mWebView.screenPxToWebPx(newSpacerHeightPx);
635 LogUtils.i(LAYOUT_TAG, "setting HTML spacer expanded=%s h=%dwebPx (%dscreenPx)",
636 item.isExpanded(), h, newSpacerHeightPx);
Vikram Aggarwal5349ce12012-09-24 14:12:40 -0700637 mWebView.loadUrl(String.format("javascript:setMessageBodyVisible('%s', %s, %s);",
Andy Huang014ea4c2012-09-25 14:50:54 -0700638 mTemplates.getMessageDomId(item.getMessage()), item.isExpanded(), h));
Andy Huang839ada22012-07-20 15:48:40 -0700639
Andy Huang014ea4c2012-09-25 14:50:54 -0700640 mViewState.setExpansionState(item.getMessage(),
Paul Westbrook08098ec2012-08-12 15:30:28 -0700641 item.isExpanded() ? ExpansionState.EXPANDED : ExpansionState.COLLAPSED);
Andy Huang3233bff2012-03-20 19:38:45 -0700642 }
643
644 @Override
645 public void showExternalResources(Message msg) {
646 mWebView.getSettings().setBlockNetworkImage(false);
647 mWebView.loadUrl("javascript:unblockImages('" + mTemplates.getMessageDomId(msg) + "');");
648 }
649 // END message header callbacks
Andy Huang5ff63742012-03-16 20:30:23 -0700650
Andy Huang46dfba62012-04-19 01:47:32 -0700651 @Override
652 public void onSuperCollapsedClick(SuperCollapsedBlockItem item) {
mindypf4fce122012-09-14 15:55:33 -0700653 MessageCursor cursor = getMessageCursor();
654 if (cursor == null || !mViewsCreated) {
Andy Huang46dfba62012-04-19 01:47:32 -0700655 return;
656 }
657
mindypf4fce122012-09-14 15:55:33 -0700658 mTempBodiesHtml = renderCollapsedHeaders(cursor, item);
Andy Huang46dfba62012-04-19 01:47:32 -0700659 mWebView.loadUrl("javascript:replaceSuperCollapsedBlock(" + item.getStart() + ")");
660 }
661
Andy Huang47aa9c92012-07-31 15:37:21 -0700662 private void showNewMessageNotification(NewMessagesInfo info) {
663 final TextView descriptionView = (TextView) mNewMessageBar.findViewById(
664 R.id.new_message_description);
665 descriptionView.setText(info.getNotificationText());
666 mNewMessageBar.setVisibility(View.VISIBLE);
667 }
668
669 private void onNewMessageBarClick() {
670 mNewMessageBar.setVisibility(View.GONE);
671
mindypf4fce122012-09-14 15:55:33 -0700672 renderConversation(getMessageCursor()); // mCursor is already up-to-date
673 // per onLoadFinished()
Andy Huang5fbda022012-02-28 18:22:03 -0800674 }
675
Andy Huangb5078b22012-03-05 19:52:29 -0800676 private static int[] parseInts(final String[] stringArray) {
677 final int len = stringArray.length;
678 final int[] ints = new int[len];
679 for (int i = 0; i < len; i++) {
680 ints[i] = Integer.parseInt(stringArray[i]);
681 }
682 return ints;
683 }
684
Andy Huang47aa9c92012-07-31 15:37:21 -0700685 @Override
686 public String toString() {
687 // log extra info at DEBUG level or finer
688 final String s = super.toString();
689 if (!LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG) || mConversation == null) {
690 return s;
691 }
692 return "(" + s + " subj=" + mConversation.subject + ")";
693 }
694
Andy Huang16174812012-08-16 16:40:35 -0700695 private Address getAddress(String rawFrom) {
696 Address addr = mAddressCache.get(rawFrom);
697 if (addr == null) {
698 addr = Address.getEmailAddress(rawFrom);
699 mAddressCache.put(rawFrom, addr);
700 }
701 return addr;
702 }
703
Andy Huang28b7aee2012-08-20 20:27:32 -0700704 @Override
705 public Account getAccount() {
706 return mAccount;
707 }
708
Paul Westbrook542fec92012-09-18 14:47:51 -0700709 private class ConversationWebViewClient extends AbstractConversationWebViewClient {
Andy Huang17a9cde2012-03-09 18:03:16 -0800710 @Override
711 public void onPageFinished(WebView view, String url) {
Andy Huangde56e972012-07-26 18:23:08 -0700712 // Ignore unsafe calls made after a fragment is detached from an activity
713 final ControllableActivity activity = (ControllableActivity) getActivity();
714 if (activity == null || !mViewsCreated) {
715 LogUtils.i(LOG_TAG, "ignoring CVF.onPageFinished, url=%s fragment=%s", url,
Andy Huangb95da852012-07-18 14:16:58 -0700716 ConversationViewFragment.this);
717 return;
718 }
719
Andy Huang47aa9c92012-07-31 15:37:21 -0700720 LogUtils.i(LOG_TAG, "IN CVF.onPageFinished, url=%s fragment=%s act=%s", url,
721 ConversationViewFragment.this, getActivity());
Andy Huang632721e2012-04-11 16:57:26 -0700722
Andy Huang17a9cde2012-03-09 18:03:16 -0800723 super.onPageFinished(view, url);
724
725 // TODO: save off individual message unread state (here, or in onLoadFinished?) so
726 // 'mark unread' restores the original unread state for each individual message
727
Andy Huang632721e2012-04-11 16:57:26 -0700728 if (mUserVisible) {
729 onConversationSeen();
Andy Huang51067132012-03-12 20:08:19 -0700730 }
mindyp3bcf1802012-09-09 11:17:00 -0700731 if (!mEnableContentReadySignal) {
732 notifyConversationLoaded(mConversation);
733 dismissLoadingStatus();
734 }
Paul Westbrook089f2622012-09-09 21:51:11 -0700735 // We are not able to use the loader manager unless this fragment is added to the
736 // activity
737 if (isAdded()) {
738 final Set<String> emailAddresses = Sets.newHashSet();
739 for (Address addr : mAddressCache.values()) {
740 emailAddresses.add(addr.getAddress());
741 }
mindypf4fce122012-09-14 15:55:33 -0700742 ContactLoaderCallbacks callbacks = getContactInfoSource();
743 getContactInfoSource().setSenders(emailAddresses);
744 getLoaderManager().restartLoader(CONTACT_LOADER, Bundle.EMPTY, callbacks);
Andy Huangb8331b42012-07-16 19:08:53 -0700745 }
Andy Huang17a9cde2012-03-09 18:03:16 -0800746 }
747
Andy Huangaf5d4e02012-03-19 19:02:12 -0700748 @Override
749 public boolean shouldOverrideUrlLoading(WebView view, String url) {
Paul Westbrook542fec92012-09-18 14:47:51 -0700750 return mViewsCreated && super.shouldOverrideUrlLoading(view, url);
Andy Huangaf5d4e02012-03-19 19:02:12 -0700751 }
Andy Huang17a9cde2012-03-09 18:03:16 -0800752 }
753
Andy Huangf70fc402012-02-17 15:37:42 -0800754 /**
mindyp3bcf1802012-09-09 11:17:00 -0700755 * Notifies the {@link ConversationViewable.ConversationCallbacks} that the conversation has
756 * been loaded.
757 */
758 public void notifyConversationLoaded(Conversation c) {
mindypdde3f9f2012-09-10 17:35:35 -0700759 if (mWebViewSizeChangeListener == null) {
760 mWebViewSizeChangeListener = new ConversationWebView.ContentSizeChangeListener() {
761 @Override
762 public void onHeightChange(int h) {
763 // When WebKit says the DOM height has changed, re-measure
764 // bodies and re-position their headers.
765 // This is separate from the typical JavaScript DOM change
766 // listeners because cases like NARROW_COLUMNS text reflow do not trigger DOM
767 // events.
768 mWebView.loadUrl("javascript:measurePositions();");
769 }
770 };
771 }
772 mWebView.setContentSizeChangeListener(mWebViewSizeChangeListener);
mindyp3bcf1802012-09-09 11:17:00 -0700773 }
774
775 /**
776 * Notifies the {@link ConversationViewable.ConversationCallbacks} that the conversation has
777 * failed to load.
778 */
779 protected void notifyConversationLoadError(Conversation c) {
780 mActivity.onConversationLoadError();
781 }
782
mindyp3bcf1802012-09-09 11:17:00 -0700783 /**
Andy Huangf70fc402012-02-17 15:37:42 -0800784 * NOTE: all public methods must be listed in the proguard flags so that they can be accessed
785 * via reflection and not stripped.
786 *
787 */
788 private class MailJsBridge {
789
790 @SuppressWarnings("unused")
Mindy Pereira974c9662012-09-14 10:02:08 -0700791 @JavascriptInterface
Andy Huang7bdc3752012-03-25 17:18:19 -0700792 public void onWebContentGeometryChange(final String[] overlayBottomStrs) {
Andy Huang46dfba62012-04-19 01:47:32 -0700793 try {
mindypff282d02012-09-17 10:33:02 -0700794 getHandler().post(new Runnable() {
Andy Huang46dfba62012-04-19 01:47:32 -0700795 @Override
796 public void run() {
797 if (!mViewsCreated) {
798 LogUtils.d(LOG_TAG, "ignoring webContentGeometryChange because views" +
799 " are gone, %s", ConversationViewFragment.this);
800 return;
801 }
Andy Huang51067132012-03-12 20:08:19 -0700802
Andy Huang46dfba62012-04-19 01:47:32 -0700803 mConversationContainer.onGeometryChange(parseInts(overlayBottomStrs));
804 }
805 });
806 } catch (Throwable t) {
807 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onWebContentGeometryChange");
808 }
809 }
810
811 @SuppressWarnings("unused")
Mindy Pereira974c9662012-09-14 10:02:08 -0700812 @JavascriptInterface
Andy Huang46dfba62012-04-19 01:47:32 -0700813 public String getTempMessageBodies() {
814 try {
815 if (!mViewsCreated) {
816 return "";
Andy Huangf70fc402012-02-17 15:37:42 -0800817 }
Andy Huang46dfba62012-04-19 01:47:32 -0700818
819 final String s = mTempBodiesHtml;
820 mTempBodiesHtml = null;
821 return s;
822 } catch (Throwable t) {
823 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getTempMessageBodies");
824 return "";
825 }
Andy Huangf70fc402012-02-17 15:37:42 -0800826 }
827
Andy Huang014ea4c2012-09-25 14:50:54 -0700828 @SuppressWarnings("unused")
829 @JavascriptInterface
830 public String getMessageBody(String domId) {
831 try {
832 final MessageCursor cursor = getMessageCursor();
833 if (!mViewsCreated || cursor == null) {
834 return "";
835 }
836
837 int pos = -1;
838 while (cursor.moveToPosition(++pos)) {
839 final ConversationMessage msg = cursor.getMessage();
840 if (TextUtils.equals(domId, mTemplates.getMessageDomId(msg))) {
841 return msg.getBodyAsHtml();
842 }
843 }
844
845 return "";
846
847 } catch (Throwable t) {
848 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getMessageBody");
849 return "";
850 }
851 }
852
mindyp3bcf1802012-09-09 11:17:00 -0700853 private void showConversation(Conversation conv) {
854 notifyConversationLoaded(conv);
855 dismissLoadingStatus();
856 }
857
858 @SuppressWarnings("unused")
Mindy Pereira974c9662012-09-14 10:02:08 -0700859 @JavascriptInterface
mindyp3bcf1802012-09-09 11:17:00 -0700860 public void onContentReady() {
861 final Conversation conv = mConversation;
862 try {
mindypff282d02012-09-17 10:33:02 -0700863 getHandler().post(new Runnable() {
mindyp3bcf1802012-09-09 11:17:00 -0700864 @Override
865 public void run() {
866 LogUtils.d(LOG_TAG, "ANIMATION STARTED, ready to draw. t=%s",
867 SystemClock.uptimeMillis());
868 showConversation(conv);
869 }
870 });
871 } catch (Throwable t) {
872 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onContentReady");
873 // Still try to show the conversation.
874 showConversation(conv);
875 }
876 }
Andy Huangf70fc402012-02-17 15:37:42 -0800877 }
878
Andy Huang47aa9c92012-07-31 15:37:21 -0700879 private class NewMessagesInfo {
880 int count;
881 String senderAddress;
882
883 /**
884 * Return the display text for the new message notification overlay. It will be formatted
885 * appropriately for a single new message vs. multiple new messages.
886 *
887 * @return display text
888 */
889 public String getNotificationText() {
mindypad0c30d2012-09-25 12:09:13 -0700890 Resources res = getResources();
Andy Huang47aa9c92012-07-31 15:37:21 -0700891 if (count > 1) {
mindypad0c30d2012-09-25 12:09:13 -0700892 return res.getString(R.string.new_incoming_messages_many, count);
Andy Huang47aa9c92012-07-31 15:37:21 -0700893 } else {
Andy Huang16174812012-08-16 16:40:35 -0700894 final Address addr = getAddress(senderAddress);
mindypad0c30d2012-09-25 12:09:13 -0700895 return res.getString(R.string.new_incoming_messages_one,
896 TextUtils.isEmpty(addr.getName()) ? addr.getAddress() : addr.getName());
Andy Huang47aa9c92012-07-31 15:37:21 -0700897 }
Andy Huang47aa9c92012-07-31 15:37:21 -0700898 }
899 }
900
mindypf4fce122012-09-14 15:55:33 -0700901 @Override
Andy Huang014ea4c2012-09-25 14:50:54 -0700902 public void onMessageCursorLoadFinished(Loader<Cursor> loader, MessageCursor newCursor,
903 MessageCursor oldCursor) {
mindypf4fce122012-09-14 15:55:33 -0700904 /*
905 * what kind of changes affect the MessageCursor? 1. new message(s) 2.
906 * read/unread state change 3. deleted message, either regular or draft
907 * 4. updated message, either from self or from others, updated in
908 * content or state or sender 5. star/unstar of message (technically
909 * similar to #1) 6. other label change Use MessageCursor.hashCode() to
910 * sort out interesting vs. no-op cursor updates.
911 */
Andy Huang014ea4c2012-09-25 14:50:54 -0700912 final boolean changed = newCursor != null && oldCursor != null
913 && newCursor.hashCode() != oldCursor.hashCode();
Andy Huangb8331b42012-07-16 19:08:53 -0700914
Andy Huang014ea4c2012-09-25 14:50:54 -0700915 if (oldCursor != null) {
916 final NewMessagesInfo info = getNewIncomingMessagesInfo(newCursor);
Andy Huangb8331b42012-07-16 19:08:53 -0700917
Andy Huang014ea4c2012-09-25 14:50:54 -0700918 if (info.count > 0) {
919 // don't immediately render new incoming messages from other
920 // senders
921 // (to avoid a new message from losing the user's focus)
922 LogUtils.i(LOG_TAG, "CONV RENDER: conversation updated"
923 + ", holding cursor for new incoming message");
924 showNewMessageNotification(info);
925 return;
926 }
927
928 if (!changed) {
929 final boolean processedInPlace = processInPlaceUpdates(newCursor, oldCursor);
930 if (processedInPlace) {
931 LogUtils.i(LOG_TAG, "CONV RENDER: processed update(s) in place");
Andy Huang1ee96b22012-08-24 20:19:53 -0700932 } else {
mindypf4fce122012-09-14 15:55:33 -0700933 LogUtils.i(LOG_TAG, "CONV RENDER: uninteresting update"
934 + ", ignoring this conversation update");
Andy Huang1ee96b22012-08-24 20:19:53 -0700935 }
Andy Huangb8331b42012-07-16 19:08:53 -0700936 return;
937 }
Andy Huangb8331b42012-07-16 19:08:53 -0700938 }
939
mindypf4fce122012-09-14 15:55:33 -0700940 // cursors are different, and not due to an incoming message. fall
941 // through and render.
942 LogUtils.i(LOG_TAG, "CONV RENDER: conversation updated"
943 + ", but not due to incoming message. rendering.");
Andy Huangb8331b42012-07-16 19:08:53 -0700944
mindypf4fce122012-09-14 15:55:33 -0700945 // TODO: if this is not user-visible, delay render until user-visible
946 // fragment is done. This is needed in addition to the
947 // showConversation() delay to speed up rotation and restoration.
Andy Huang014ea4c2012-09-25 14:50:54 -0700948 renderConversation(newCursor);
Andy Huangb8331b42012-07-16 19:08:53 -0700949 }
950
mindypf4fce122012-09-14 15:55:33 -0700951 private NewMessagesInfo getNewIncomingMessagesInfo(MessageCursor newCursor) {
952 final NewMessagesInfo info = new NewMessagesInfo();
Andy Huangb8331b42012-07-16 19:08:53 -0700953
mindypf4fce122012-09-14 15:55:33 -0700954 int pos = -1;
955 while (newCursor.moveToPosition(++pos)) {
956 final Message m = newCursor.getMessage();
957 if (!mViewState.contains(m)) {
958 LogUtils.i(LOG_TAG, "conversation diff: found new msg: %s", m.uri);
Andy Huangb8331b42012-07-16 19:08:53 -0700959
mindypf4fce122012-09-14 15:55:33 -0700960 final Address from = getAddress(m.from);
961 // distinguish ours from theirs
962 // new messages from the account owner should not trigger a
963 // notification
964 if (mAccount.ownsFromAddress(from.getAddress())) {
965 LogUtils.i(LOG_TAG, "found message from self: %s", m.uri);
966 continue;
967 }
Andy Huangb8331b42012-07-16 19:08:53 -0700968
mindypf4fce122012-09-14 15:55:33 -0700969 info.count++;
970 info.senderAddress = m.from;
Andy Huangb8331b42012-07-16 19:08:53 -0700971 }
Andy Huangb8331b42012-07-16 19:08:53 -0700972 }
mindypf4fce122012-09-14 15:55:33 -0700973 return info;
Andy Huangb8331b42012-07-16 19:08:53 -0700974 }
975
Andy Huang014ea4c2012-09-25 14:50:54 -0700976 private boolean processInPlaceUpdates(MessageCursor newCursor, MessageCursor oldCursor) {
977 final Set<String> idsOfChangedBodies = Sets.newHashSet();
978 boolean changed = false;
979
980 int pos = 0;
981 while (true) {
982 if (!newCursor.moveToPosition(pos) || !oldCursor.moveToPosition(pos)) {
983 break;
984 }
985
986 final ConversationMessage newMsg = newCursor.getMessage();
987 final ConversationMessage oldMsg = oldCursor.getMessage();
988
989 if (!TextUtils.equals(newMsg.from, oldMsg.from)) {
990 mAdapter.updateItemsForMessage(newMsg);
991 LogUtils.i(LOG_TAG, "msg #%d (%d): detected sender change", pos, newMsg.id);
992 changed = true;
993 }
994
995 // update changed message bodies in-place
996 if (!TextUtils.equals(newMsg.bodyHtml, oldMsg.bodyHtml) ||
997 !TextUtils.equals(newMsg.bodyText, oldMsg.bodyText)) {
998 // maybe just set a flag to notify JS to re-request changed bodies
999 idsOfChangedBodies.add('"' + mTemplates.getMessageDomId(newMsg) + '"');
1000 LogUtils.i(LOG_TAG, "msg #%d (%d): detected body change", pos, newMsg.id);
1001 }
1002
1003 pos++;
1004 }
1005
1006 if (!idsOfChangedBodies.isEmpty()) {
1007 mWebView.loadUrl(String.format("javascript:replaceMessageBodies([%s]);",
1008 TextUtils.join(",", idsOfChangedBodies)));
1009 changed = true;
1010 }
1011
1012 return changed;
1013 }
1014
Paul Westbrookcebcc642012-08-08 10:06:04 -07001015 private class SetCookieTask extends AsyncTask<Void, Void, Void> {
1016 final String mUri;
1017 final String mCookie;
1018
1019 SetCookieTask(String uri, String cookie) {
1020 mUri = uri;
1021 mCookie = cookie;
1022 }
1023
1024 @Override
1025 public Void doInBackground(Void... args) {
1026 final CookieSyncManager csm =
mindypf4fce122012-09-14 15:55:33 -07001027 CookieSyncManager.createInstance(getContext());
Paul Westbrookcebcc642012-08-08 10:06:04 -07001028 CookieManager.getInstance().setCookie(mUri, mCookie);
1029 csm.sync();
1030 return null;
1031 }
1032 }
mindyp36280f32012-09-09 16:11:23 -07001033
mindyp26d4d2d2012-09-18 17:30:32 -07001034 @Override
mindyp36280f32012-09-09 16:11:23 -07001035 public void onConversationUpdated(Conversation conv) {
1036 final ConversationViewHeader headerView = (ConversationViewHeader) mConversationContainer
1037 .findViewById(R.id.conversation_header);
mindypb2b98ba2012-09-24 14:13:58 -07001038 mConversation = conv;
mindyp9e0b2362012-09-09 16:31:21 -07001039 if (headerView != null) {
1040 headerView.onConversationUpdated(conv);
1041 }
mindyp36280f32012-09-09 16:11:23 -07001042 }
Mindy Pereira9b875682012-02-15 18:10:54 -08001043}