blob: 2220f2f95c3094f0e3c0b778903b6220591c0a81 [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
Paul Westbrookb8361c92012-09-27 10:57:14 -070021import android.content.ContentResolver;
Mindy Pereira9b875682012-02-15 18:10:54 -080022import android.content.Context;
Mindy Pereira8e915722012-02-16 14:42:56 -080023import android.content.Loader;
mindypad0c30d2012-09-25 12:09:13 -070024import android.content.res.Resources;
Mindy Pereira9b875682012-02-15 18:10:54 -080025import android.database.Cursor;
Andy Huang9d3fd922012-09-26 22:23:58 -070026import android.database.DataSetObserver;
Paul Westbrookb8361c92012-09-27 10:57:14 -070027import android.net.Uri;
Paul Westbrookcebcc642012-08-08 10:06:04 -070028import android.os.AsyncTask;
Mindy Pereira9b875682012-02-15 18:10:54 -080029import android.os.Bundle;
mindyp3bcf1802012-09-09 11:17:00 -070030import android.os.SystemClock;
Andy Huang47aa9c92012-07-31 15:37:21 -070031import android.text.TextUtils;
Mindy Pereira9b875682012-02-15 18:10:54 -080032import android.view.LayoutInflater;
Andy Huang02f9d182012-11-28 22:38:02 -080033import android.view.ScaleGestureDetector;
Andy Huang02f9d182012-11-28 22:38:02 -080034import android.view.ScaleGestureDetector.OnScaleGestureListener;
Andy Huangc1fb9a92013-02-11 13:09:12 -080035import android.view.View;
Mark Wei4071c2f2012-09-26 14:38:38 -070036import android.view.View.OnLayoutChangeListener;
Mindy Pereira9b875682012-02-15 18:10:54 -080037import android.view.ViewGroup;
Andy Huangf70fc402012-02-17 15:37:42 -080038import android.webkit.ConsoleMessage;
Paul Westbrookcebcc642012-08-08 10:06:04 -070039import android.webkit.CookieManager;
40import android.webkit.CookieSyncManager;
Mindy Pereira974c9662012-09-14 10:02:08 -070041import android.webkit.JavascriptInterface;
Andy Huangf70fc402012-02-17 15:37:42 -080042import android.webkit.WebChromeClient;
43import android.webkit.WebSettings;
Andy Huang17a9cde2012-03-09 18:03:16 -080044import android.webkit.WebView;
45import android.webkit.WebViewClient;
Andy Huang47aa9c92012-07-31 15:37:21 -070046import android.widget.TextView;
Mindy Pereira9b875682012-02-15 18:10:54 -080047
Andy Huang59e0b182012-08-14 14:32:23 -070048import com.android.mail.FormattedDateBuilder;
Mindy Pereira9b875682012-02-15 18:10:54 -080049import com.android.mail.R;
Andy Huang5ff63742012-03-16 20:30:23 -070050import com.android.mail.browse.ConversationContainer;
Andy Huangadbf3e82012-10-13 13:30:19 -070051import com.android.mail.browse.ConversationContainer.OverlayPosition;
Andy Huang46dfba62012-04-19 01:47:32 -070052import com.android.mail.browse.ConversationOverlayItem;
Andy Huang7bdc3752012-03-25 17:18:19 -070053import com.android.mail.browse.ConversationViewAdapter;
Andy Huang46dfba62012-04-19 01:47:32 -070054import com.android.mail.browse.ConversationViewAdapter.MessageFooterItem;
Andy Huang7bdc3752012-03-25 17:18:19 -070055import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem;
Andy Huang46dfba62012-04-19 01:47:32 -070056import com.android.mail.browse.ConversationViewAdapter.SuperCollapsedBlockItem;
Andy Huang5ff63742012-03-16 20:30:23 -070057import com.android.mail.browse.ConversationViewHeader;
58import com.android.mail.browse.ConversationWebView;
Andy Huangc1fb9a92013-02-11 13:09:12 -080059import com.android.mail.browse.MailWebView.ContentSizeChangeListener;
Andy Huang7bdc3752012-03-25 17:18:19 -070060import com.android.mail.browse.MessageCursor;
Andy Huang28b7aee2012-08-20 20:27:32 -070061import com.android.mail.browse.MessageCursor.ConversationMessage;
Andy Huang59e0b182012-08-14 14:32:23 -070062import com.android.mail.browse.MessageHeaderView;
Andy Huangadbf3e82012-10-13 13:30:19 -070063import com.android.mail.browse.ScrollIndicatorsView;
Andy Huang46dfba62012-04-19 01:47:32 -070064import com.android.mail.browse.SuperCollapsedBlock;
Andy Huang0b7ed6f2012-07-25 19:23:26 -070065import com.android.mail.browse.WebViewContextMenu;
Paul Westbrookc42ad5e2013-05-09 16:52:15 -070066import com.android.mail.content.ObjectCursor;
Andy Huangc1fb9a92013-02-11 13:09:12 -080067import com.android.mail.preferences.MailPrefs;
Mindy Pereira9b875682012-02-15 18:10:54 -080068import com.android.mail.providers.Account;
Andy Huang65fe28f2012-04-06 18:08:53 -070069import com.android.mail.providers.Address;
Mindy Pereira9b875682012-02-15 18:10:54 -080070import com.android.mail.providers.Conversation;
Andy Huangf70fc402012-02-17 15:37:42 -080071import com.android.mail.providers.Message;
Paul Westbrookb8361c92012-09-27 10:57:14 -070072import com.android.mail.providers.UIProvider;
Andy Huangcd5c5ee2012-08-12 19:03:51 -070073import com.android.mail.ui.ConversationViewState.ExpansionState;
Paul Westbrookb334c902012-06-25 11:42:46 -070074import com.android.mail.utils.LogTag;
Mindy Pereira9b875682012-02-15 18:10:54 -080075import com.android.mail.utils.LogUtils;
Andy Huang2e9acfe2012-03-15 22:39:36 -070076import com.android.mail.utils.Utils;
Andy Huang543e7092013-04-22 11:44:56 -070077import com.google.common.collect.ImmutableList;
Andy Huang46dfba62012-04-19 01:47:32 -070078import com.google.common.collect.Lists;
Andy Huang05c70c82013-03-14 15:15:50 -070079import com.google.common.collect.Maps;
Andy Huangb8331b42012-07-16 19:08:53 -070080import com.google.common.collect.Sets;
Andy Huang65fe28f2012-04-06 18:08:53 -070081
Scott Kennedyeb9a4bd2012-11-12 10:33:04 -080082import java.util.ArrayList;
Andy Huang46dfba62012-04-19 01:47:32 -070083import java.util.List;
Andy Huang05c70c82013-03-14 15:15:50 -070084import java.util.Map;
Andy Huangb8331b42012-07-16 19:08:53 -070085import java.util.Set;
Mindy Pereira9b875682012-02-15 18:10:54 -080086
Andy Huangf70fc402012-02-17 15:37:42 -080087
Mindy Pereira9b875682012-02-15 18:10:54 -080088/**
89 * The conversation view UI component.
90 */
mindypf4fce122012-09-14 15:55:33 -070091public final class ConversationViewFragment extends AbstractConversationViewFragment implements
Andy Huangcd5c5ee2012-08-12 19:03:51 -070092 SuperCollapsedBlock.OnClickListener,
Andy Huang57f354c2013-04-11 17:23:40 -070093 OnLayoutChangeListener {
Mindy Pereira8e915722012-02-16 14:42:56 -080094
Paul Westbrookb334c902012-06-25 11:42:46 -070095 private static final String LOG_TAG = LogTag.getLogTag();
Andy Huang632721e2012-04-11 16:57:26 -070096 public static final String LAYOUT_TAG = "ConvLayout";
Mindy Pereira9b875682012-02-15 18:10:54 -080097
Andy Huang3c276bf2013-02-19 14:24:43 -080098 private static final boolean ENABLE_CSS_ZOOM = false;
99
Andy Huang9d3fd922012-09-26 22:23:58 -0700100 /**
mindyp1b3cc472012-09-27 11:32:59 -0700101 * Difference in the height of the message header whose details have been expanded/collapsed
102 */
103 private int mDiff = 0;
104
105 /**
Andy Huang9d3fd922012-09-26 22:23:58 -0700106 * Default value for {@link #mLoadWaitReason}. Conversation load will happen immediately.
107 */
108 private final int LOAD_NOW = 0;
109 /**
110 * Value for {@link #mLoadWaitReason} that means we are offscreen and waiting for the visible
111 * conversation to finish loading before beginning our load.
112 * <p>
113 * When this value is set, the fragment should register with {@link ConversationListCallbacks}
114 * to know when the visible conversation is loaded. When it is unset, it should unregister.
115 */
116 private final int LOAD_WAIT_FOR_INITIAL_CONVERSATION = 1;
117 /**
118 * Value for {@link #mLoadWaitReason} used when a conversation is too heavyweight to load at
119 * all when not visible (e.g. requires network fetch, or too complex). Conversation load will
120 * wait until this fragment is visible.
121 */
122 private final int LOAD_WAIT_UNTIL_VISIBLE = 2;
mindyp3bcf1802012-09-09 11:17:00 -0700123
Andy Huangf70fc402012-02-17 15:37:42 -0800124 private ConversationContainer mConversationContainer;
Mindy Pereira9b875682012-02-15 18:10:54 -0800125
Andy Huangf70fc402012-02-17 15:37:42 -0800126 private ConversationWebView mWebView;
Mindy Pereira9b875682012-02-15 18:10:54 -0800127
Mark Wei56d83852012-09-19 14:28:50 -0700128 private ScrollIndicatorsView mScrollIndicators;
129
Andy Huang47aa9c92012-07-31 15:37:21 -0700130 private View mNewMessageBar;
131
Andy Huangf70fc402012-02-17 15:37:42 -0800132 private HtmlConversationTemplates mTemplates;
133
Andy Huangf70fc402012-02-17 15:37:42 -0800134 private final MailJsBridge mJsBridge = new MailJsBridge();
135
Andy Huang17a9cde2012-03-09 18:03:16 -0800136 private final WebViewClient mWebViewClient = new ConversationWebViewClient();
137
Andy Huang7bdc3752012-03-25 17:18:19 -0700138 private ConversationViewAdapter mAdapter;
Andy Huang51067132012-03-12 20:08:19 -0700139
140 private boolean mViewsCreated;
Mark Wei4071c2f2012-09-26 14:38:38 -0700141 // True if we attempted to render before the views were laid out
142 // We will render immediately once layout is done
143 private boolean mNeedRender;
Andy Huang51067132012-03-12 20:08:19 -0700144
Andy Huang46dfba62012-04-19 01:47:32 -0700145 /**
146 * Temporary string containing the message bodies of the messages within a super-collapsed
147 * block, for one-time use during block expansion. We cannot easily pass the body HTML
148 * into JS without problematic escaping, so hold onto it momentarily and signal JS to fetch it
149 * using {@link MailJsBridge}.
150 */
151 private String mTempBodiesHtml;
152
Andy Huang632721e2012-04-11 16:57:26 -0700153 private int mMaxAutoLoadMessages;
154
Andy Huang02f9d182012-11-28 22:38:02 -0800155 private int mSideMarginPx;
156
Andy Huang9d3fd922012-09-26 22:23:58 -0700157 /**
158 * If this conversation fragment is not visible, and it's inappropriate to load up front,
159 * this is the reason we are waiting. This flag should be cleared once it's okay to load
160 * the conversation.
161 */
162 private int mLoadWaitReason = LOAD_NOW;
Andy Huang632721e2012-04-11 16:57:26 -0700163
mindyp3bcf1802012-09-09 11:17:00 -0700164 private boolean mEnableContentReadySignal;
Andy Huang28b7aee2012-08-20 20:27:32 -0700165
mindypdde3f9f2012-09-10 17:35:35 -0700166 private ContentSizeChangeListener mWebViewSizeChangeListener;
167
Andy Huange964eee2012-10-02 19:24:58 -0700168 private float mWebViewYPercent;
169
170 /**
171 * Has loadData been called on the WebView yet?
172 */
173 private boolean mWebViewLoadedData;
174
Andy Huang63b3c672012-10-05 19:27:28 -0700175 private long mWebViewLoadStartMs;
176
Andy Huang05c70c82013-03-14 15:15:50 -0700177 private final Map<String, String> mMessageTransforms = Maps.newHashMap();
178
Andy Huang9d3fd922012-09-26 22:23:58 -0700179 private final DataSetObserver mLoadedObserver = new DataSetObserver() {
180 @Override
181 public void onChanged() {
182 getHandler().post(new FragmentRunnable("delayedConversationLoad") {
183 @Override
184 public void go() {
185 LogUtils.d(LOG_TAG, "CVF load observer fired, this=%s",
186 ConversationViewFragment.this);
187 handleDelayedConversationLoad();
188 }
189 });
190 }
191 };
Andy Huangf70fc402012-02-17 15:37:42 -0800192
Andy Huang30bcfe72012-10-18 18:09:03 -0700193 private final Runnable mOnProgressDismiss = new FragmentRunnable("onProgressDismiss") {
Andy Huang7d4746e2012-10-17 17:03:17 -0700194 @Override
195 public void go() {
Scott Kennedy58192e52013-05-08 16:35:57 -0700196 LogUtils.d(LOG_TAG, "onProgressDismiss go() - isUserVisible() = %b", isUserVisible());
Andy Huang7d4746e2012-10-17 17:03:17 -0700197 if (isUserVisible()) {
198 onConversationSeen();
199 }
Andy Huang30bcfe72012-10-18 18:09:03 -0700200 mWebView.onRenderComplete();
Andy Huang7d4746e2012-10-17 17:03:17 -0700201 }
202 };
203
Andy Huangbd544e32012-05-29 15:56:51 -0700204 private static final boolean DEBUG_DUMP_CONVERSATION_HTML = false;
Andy Huang47aa9c92012-07-31 15:37:21 -0700205 private static final boolean DISABLE_OFFSCREEN_LOADING = false;
Andy Huang06c03622012-10-22 18:59:45 -0700206 private static final boolean DEBUG_DUMP_CURSOR_CONTENTS = false;
Andy Huange964eee2012-10-02 19:24:58 -0700207
208 private static final String BUNDLE_KEY_WEBVIEW_Y_PERCENT =
209 ConversationViewFragment.class.getName() + "webview-y-percent";
Andy Huangbd544e32012-05-29 15:56:51 -0700210
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800211 /**
212 * Constructor needs to be public to handle orientation changes and activity lifecycle events.
213 */
Andy Huangf70fc402012-02-17 15:37:42 -0800214 public ConversationViewFragment() {
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800215 super();
Mindy Pereira9b875682012-02-15 18:10:54 -0800216 }
217
218 /**
219 * Creates a new instance of {@link ConversationViewFragment}, initialized
Andy Huang632721e2012-04-11 16:57:26 -0700220 * to display a conversation with other parameters inherited/copied from an existing bundle,
221 * typically one created using {@link #makeBasicArgs}.
222 */
223 public static ConversationViewFragment newInstance(Bundle existingArgs,
224 Conversation conversation) {
225 ConversationViewFragment f = new ConversationViewFragment();
226 Bundle args = new Bundle(existingArgs);
227 args.putParcelable(ARG_CONVERSATION, conversation);
228 f.setArguments(args);
229 return f;
230 }
231
mindypf4fce122012-09-14 15:55:33 -0700232 @Override
Andy Huangadbf3e82012-10-13 13:30:19 -0700233 public void onAccountChanged(Account newAccount, Account oldAccount) {
234 // if overview mode has changed, re-render completely (no need to also update headers)
235 if (isOverviewMode(newAccount) != isOverviewMode(oldAccount)) {
236 setupOverviewMode();
237 final MessageCursor c = getMessageCursor();
238 if (c != null) {
239 renderConversation(c);
240 } else {
241 // Null cursor means this fragment is either waiting to load or in the middle of
242 // loading. Either way, a future render will happen anyway, and the new setting
243 // will take effect when that happens.
244 }
245 return;
246 }
247
mindypf4fce122012-09-14 15:55:33 -0700248 // settings may have been updated; refresh views that are known to
249 // depend on settings
mindypf4fce122012-09-14 15:55:33 -0700250 mAdapter.notifyDataSetChanged();
Andy Huang632721e2012-04-11 16:57:26 -0700251 }
252
Mindy Pereira9b875682012-02-15 18:10:54 -0800253 @Override
254 public void onActivityCreated(Bundle savedInstanceState) {
Andy Huang9d3fd922012-09-26 22:23:58 -0700255 LogUtils.d(LOG_TAG, "IN CVF.onActivityCreated, this=%s visible=%s", this, isUserVisible());
Mindy Pereira9b875682012-02-15 18:10:54 -0800256 super.onActivityCreated(savedInstanceState);
Mark Wei1abfcaf2012-09-27 11:11:07 -0700257
258 if (mActivity == null || mActivity.isFinishing()) {
259 // Activity is finishing, just bail.
260 return;
261 }
262
mindypf4fce122012-09-14 15:55:33 -0700263 Context context = getContext();
264 mTemplates = new HtmlConversationTemplates(context);
Andy Huang59e0b182012-08-14 14:32:23 -0700265
mindypf4fce122012-09-14 15:55:33 -0700266 final FormattedDateBuilder dateBuilder = new FormattedDateBuilder(context);
Andy Huang59e0b182012-08-14 14:32:23 -0700267
Paul Westbrook8081df42012-09-10 15:43:36 -0700268 mAdapter = new ConversationViewAdapter(mActivity, this,
mindypf4fce122012-09-14 15:55:33 -0700269 getLoaderManager(), this, getContactInfoSource(), this,
Paul Westbrook8081df42012-09-10 15:43:36 -0700270 this, mAddressCache, dateBuilder);
Andy Huang51067132012-03-12 20:08:19 -0700271 mConversationContainer.setOverlayAdapter(mAdapter);
272
Andy Huang59e0b182012-08-14 14:32:23 -0700273 // set up snap header (the adapter usually does this with the other ones)
274 final MessageHeaderView snapHeader = mConversationContainer.getSnapHeader();
Andy Huangc1fb9a92013-02-11 13:09:12 -0800275 initHeaderView(snapHeader, dateBuilder);
276
Andy Huang632721e2012-04-11 16:57:26 -0700277 mMaxAutoLoadMessages = getResources().getInteger(R.integer.max_auto_load_messages);
278
Andy Huang02f9d182012-11-28 22:38:02 -0800279 mSideMarginPx = getResources().getDimensionPixelOffset(
280 R.dimen.conversation_view_margin_side) + getResources().getDimensionPixelOffset(
281 R.dimen.conversation_message_content_margin_side);
282
mindypf4fce122012-09-14 15:55:33 -0700283 mWebView.setOnCreateContextMenuListener(new WebViewContextMenu(getActivity()));
Andy Huang0b7ed6f2012-07-25 19:23:26 -0700284
Andy Huangadbf3e82012-10-13 13:30:19 -0700285 // set this up here instead of onCreateView to ensure the latest Account is loaded
286 setupOverviewMode();
287
Andy Huang9d3fd922012-09-26 22:23:58 -0700288 // Defer the call to initLoader with a Handler.
289 // We want to wait until we know which fragments are present and their final visibility
290 // states before going off and doing work. This prevents extraneous loading from occurring
291 // as the ViewPager shifts about before the initial position is set.
292 //
293 // e.g. click on item #10
294 // ViewPager.setAdapter() actually first loads #0 and #1 under the assumption that #0 is
295 // the initial primary item
296 // Then CPC immediately sets the primary item to #10, which tears down #0/#1 and sets up
297 // #9/#10/#11.
298 getHandler().post(new FragmentRunnable("showConversation") {
299 @Override
300 public void go() {
301 showConversation();
302 }
303 });
Paul Westbrookcebcc642012-08-08 10:06:04 -0700304
305 if (mConversation.conversationBaseUri != null &&
Paul Westbrookb8361c92012-09-27 10:57:14 -0700306 !Utils.isEmpty(mAccount.accoutCookieQueryUri)) {
Paul Westbrookcebcc642012-08-08 10:06:04 -0700307 // Set the cookie for this base url
Paul Westbrookb8361c92012-09-27 10:57:14 -0700308 new SetCookieTask(getContext(), mConversation.conversationBaseUri,
309 mAccount.accoutCookieQueryUri).execute();
Paul Westbrookcebcc642012-08-08 10:06:04 -0700310 }
Mindy Pereira9b875682012-02-15 18:10:54 -0800311 }
312
Andy Huangc1fb9a92013-02-11 13:09:12 -0800313 private void initHeaderView(MessageHeaderView headerView, FormattedDateBuilder dateBuilder) {
314 headerView.initialize(dateBuilder, this, mAddressCache);
315 headerView.setCallbacks(this);
316 headerView.setContactInfoSource(getContactInfoSource());
317 headerView.setVeiledMatcher(mActivity.getAccountController().getVeiledAddressMatcher());
318 }
319
Mindy Pereira9b875682012-02-15 18:10:54 -0800320 @Override
Andy Huange964eee2012-10-02 19:24:58 -0700321 public void onCreate(Bundle savedState) {
322 super.onCreate(savedState);
323
324 if (savedState != null) {
325 mWebViewYPercent = savedState.getFloat(BUNDLE_KEY_WEBVIEW_Y_PERCENT);
326 }
327 }
328
329 @Override
Mindy Pereira9b875682012-02-15 18:10:54 -0800330 public View onCreateView(LayoutInflater inflater,
331 ViewGroup container, Bundle savedInstanceState) {
Andy Huang839ada22012-07-20 15:48:40 -0700332
Andy Huang632721e2012-04-11 16:57:26 -0700333 View rootView = inflater.inflate(R.layout.conversation_view, container, false);
Andy Huangf70fc402012-02-17 15:37:42 -0800334 mConversationContainer = (ConversationContainer) rootView
335 .findViewById(R.id.conversation_container);
Andy Huang8f187782012-11-06 17:49:25 -0800336 mConversationContainer.setAccountController(this);
Andy Huang47aa9c92012-07-31 15:37:21 -0700337
338 mNewMessageBar = mConversationContainer.findViewById(R.id.new_message_notification_bar);
339 mNewMessageBar.setOnClickListener(new View.OnClickListener() {
340 @Override
341 public void onClick(View v) {
342 onNewMessageBarClick();
343 }
344 });
345
mindypff282d02012-09-17 10:33:02 -0700346 instantiateProgressIndicators(rootView);
mindyp3bcf1802012-09-09 11:17:00 -0700347
Andy Huang5ff63742012-03-16 20:30:23 -0700348 mWebView = (ConversationWebView) mConversationContainer.findViewById(R.id.webview);
Andy Huangf70fc402012-02-17 15:37:42 -0800349
Andy Huangf70fc402012-02-17 15:37:42 -0800350 mWebView.addJavascriptInterface(mJsBridge, "mail");
mindyp3bcf1802012-09-09 11:17:00 -0700351 // On JB or newer, we use the 'webkitAnimationStart' DOM event to signal load complete
352 // Below JB, try to speed up initial render by having the webview do supplemental draws to
353 // custom a software canvas.
mindypb941fdb2012-09-11 08:28:23 -0700354 // TODO(mindyp):
355 //PAGE READINESS SIGNAL FOR JELLYBEAN AND NEWER
356 // Notify the app on 'webkitAnimationStart' of a simple dummy element with a simple no-op
357 // animation that immediately runs on page load. The app uses this as a signal that the
358 // content is loaded and ready to draw, since WebView delays firing this event until the
359 // layers are composited and everything is ready to draw.
360 // This signal does not seem to be reliable, so just use the old method for now.
mindyp32d911f2012-09-24 15:14:22 -0700361 mEnableContentReadySignal = Utils.isRunningJellybeanOrLater();
mindypafc9b362012-09-25 09:20:47 -0700362 mWebView.setUseSoftwareLayer(!mEnableContentReadySignal);
Andy Huang30bcfe72012-10-18 18:09:03 -0700363 mWebView.onUserVisibilityChanged(isUserVisible());
Andy Huang17a9cde2012-03-09 18:03:16 -0800364 mWebView.setWebViewClient(mWebViewClient);
Andy Huangc1fb9a92013-02-11 13:09:12 -0800365 final WebChromeClient wcc = new WebChromeClient() {
Andy Huangf70fc402012-02-17 15:37:42 -0800366 @Override
367 public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
Andy Huang5ea5a832013-03-07 16:49:09 -0800368 LogUtils.i(LOG_TAG, "JS: %s (%s:%d) f=%s", consoleMessage.message(),
369 consoleMessage.sourceId(), consoleMessage.lineNumber(),
370 ConversationViewFragment.this);
Andy Huangf70fc402012-02-17 15:37:42 -0800371 return true;
372 }
Andy Huangc1fb9a92013-02-11 13:09:12 -0800373 };
374 mWebView.setWebChromeClient(wcc);
Andy Huangf70fc402012-02-17 15:37:42 -0800375
Andy Huang3233bff2012-03-20 19:38:45 -0700376 final WebSettings settings = mWebView.getSettings();
Andy Huangf70fc402012-02-17 15:37:42 -0800377
Mark Wei56d83852012-09-19 14:28:50 -0700378 mScrollIndicators = (ScrollIndicatorsView) rootView.findViewById(R.id.scroll_indicators);
379 mScrollIndicators.setSourceView(mWebView);
380
Andy Huangf70fc402012-02-17 15:37:42 -0800381 settings.setJavaScriptEnabled(true);
Andy Huangf70fc402012-02-17 15:37:42 -0800382
Andy Huangc319b552012-04-25 19:53:50 -0700383 final float fontScale = getResources().getConfiguration().fontScale;
Andy Huangba283732012-06-25 19:14:10 -0700384 final int desiredFontSizePx = getResources()
385 .getInteger(R.integer.conversation_desired_font_size_px);
386 final int unstyledFontSizePx = getResources()
387 .getInteger(R.integer.conversation_unstyled_font_size_px);
Andy Huangc319b552012-04-25 19:53:50 -0700388
Andy Huangba283732012-06-25 19:14:10 -0700389 int textZoom = settings.getTextZoom();
390 // apply a correction to the default body text style to get regular text to the size we want
391 textZoom = textZoom * desiredFontSizePx / unstyledFontSizePx;
392 // then apply any system font scaling
Andy Huangc319b552012-04-25 19:53:50 -0700393 textZoom = (int) (textZoom * fontScale);
394 settings.setTextZoom(textZoom);
395
Andy Huang51067132012-03-12 20:08:19 -0700396 mViewsCreated = true;
Andy Huange964eee2012-10-02 19:24:58 -0700397 mWebViewLoadedData = false;
Andy Huang51067132012-03-12 20:08:19 -0700398
Mindy Pereira9b875682012-02-15 18:10:54 -0800399 return rootView;
400 }
401
402 @Override
403 public void onDestroyView() {
Mindy Pereira9b875682012-02-15 18:10:54 -0800404 super.onDestroyView();
Andy Huang46dfba62012-04-19 01:47:32 -0700405 mConversationContainer.setOverlayAdapter(null);
406 mAdapter = null;
Andy Huang9d3fd922012-09-26 22:23:58 -0700407 resetLoadWaiting(); // be sure to unregister any active load observer
Andy Huang51067132012-03-12 20:08:19 -0700408 mViewsCreated = false;
Mindy Pereira9b875682012-02-15 18:10:54 -0800409 }
410
Andy Huange964eee2012-10-02 19:24:58 -0700411 @Override
Scott Kennedyfafcd172012-11-01 17:23:33 -0700412 protected WebView getWebView() {
413 return mWebView;
414 }
415
416 @Override
Andy Huange964eee2012-10-02 19:24:58 -0700417 public void onSaveInstanceState(Bundle outState) {
418 super.onSaveInstanceState(outState);
419
420 outState.putFloat(BUNDLE_KEY_WEBVIEW_Y_PERCENT, calculateScrollYPercent());
421 }
422
423 private float calculateScrollYPercent() {
Paul Westbrook1b56a672013-04-19 01:19:05 -0700424 final float p;
425 if (mWebView == null) {
426 // onCreateView hasn't been called, return 0 as the user hasn't scrolled the view.
427 return 0;
428 }
429
430 final int scrollY = mWebView.getScrollY();
431 final int viewH = mWebView.getHeight();
432 final int webH = (int) (mWebView.getContentHeight() * mWebView.getScale());
Andy Huange964eee2012-10-02 19:24:58 -0700433
434 if (webH == 0 || webH <= viewH) {
435 p = 0;
436 } else if (scrollY + viewH >= webH) {
437 // The very bottom is a special case, it acts as a stronger anchor than the scroll top
438 // at that point.
439 p = 1.0f;
440 } else {
441 p = (float) scrollY / webH;
442 }
443 return p;
444 }
445
Andy Huang9d3fd922012-09-26 22:23:58 -0700446 private void resetLoadWaiting() {
447 if (mLoadWaitReason == LOAD_WAIT_FOR_INITIAL_CONVERSATION) {
448 getListController().unregisterConversationLoadedObserver(mLoadedObserver);
449 }
450 mLoadWaitReason = LOAD_NOW;
451 }
452
Andy Huang5ff63742012-03-16 20:30:23 -0700453 @Override
mindypf4fce122012-09-14 15:55:33 -0700454 protected void markUnread() {
Vikram Aggarwald82a31f2013-02-05 15:03:00 -0800455 super.markUnread();
Andy Huang839ada22012-07-20 15:48:40 -0700456 // Ignore unsafe calls made after a fragment is detached from an activity
457 final ControllableActivity activity = (ControllableActivity) getActivity();
458 if (activity == null) {
459 LogUtils.w(LOG_TAG, "ignoring markUnread for conv=%s", mConversation.id);
460 return;
461 }
462
Andy Huang28e31e22012-07-26 16:33:15 -0700463 if (mViewState == null) {
464 LogUtils.i(LOG_TAG, "ignoring markUnread for conv with no view state (%d)",
465 mConversation.id);
466 return;
467 }
Andy Huang839ada22012-07-20 15:48:40 -0700468 activity.getConversationUpdater().markConversationMessagesUnread(mConversation,
Vikram Aggarwal4a878b62012-07-31 15:09:25 -0700469 mViewState.getUnreadMessageUris(), mViewState.getConversationInfo());
Andy Huang839ada22012-07-20 15:48:40 -0700470 }
471
mindypf4fce122012-09-14 15:55:33 -0700472 @Override
473 public void onUserVisibleHintChanged() {
Andy Huang9d3fd922012-09-26 22:23:58 -0700474 final boolean userVisible = isUserVisible();
Scott Kennedy58192e52013-05-08 16:35:57 -0700475 LogUtils.d(LOG_TAG, "ConversationViewFragment#onUserVisibleHintChanged(), userVisible = %b",
476 userVisible);
Andy Huang9d3fd922012-09-26 22:23:58 -0700477
478 if (!userVisible) {
mindyp32d911f2012-09-24 15:14:22 -0700479 dismissLoadingStatus();
Andy Huang9d3fd922012-09-26 22:23:58 -0700480 } else if (mViewsCreated) {
481 if (getMessageCursor() != null) {
482 LogUtils.d(LOG_TAG, "Fragment is now user-visible, onConversationSeen: %s", this);
483 onConversationSeen();
484 } else if (isLoadWaiting()) {
485 LogUtils.d(LOG_TAG, "Fragment is now user-visible, showing conversation: %s", this);
486 handleDelayedConversationLoad();
487 }
Andy Huang632721e2012-04-11 16:57:26 -0700488 }
Andy Huang632721e2012-04-11 16:57:26 -0700489
Andy Huang30bcfe72012-10-18 18:09:03 -0700490 if (mWebView != null) {
491 mWebView.onUserVisibilityChanged(userVisible);
492 }
Andy Huangf8cf5462012-10-17 18:29:14 -0700493 }
494
Andy Huang9d3fd922012-09-26 22:23:58 -0700495 /**
496 * Will either call initLoader now to begin loading, or set {@link #mLoadWaitReason} and do
497 * nothing (in which case you should later call {@link #handleDelayedConversationLoad()}).
498 */
Mindy Pereira9b875682012-02-15 18:10:54 -0800499 private void showConversation() {
Andy Huang9d3fd922012-09-26 22:23:58 -0700500 final int reason;
501
502 if (isUserVisible()) {
503 LogUtils.i(LOG_TAG,
504 "SHOWCONV: CVF is user-visible, immediately loading conversation (%s)", this);
505 reason = LOAD_NOW;
Andy Huang243c2362013-03-01 17:50:35 -0800506 timerMark("CVF.showConversation");
Andy Huang9d3fd922012-09-26 22:23:58 -0700507 } else {
508 final boolean disableOffscreenLoading = DISABLE_OFFSCREEN_LOADING
509 || (mConversation.isRemote
510 || mConversation.getNumMessages() > mMaxAutoLoadMessages);
511
512 // When not visible, we should not immediately load if either this conversation is
513 // too heavyweight, or if the main/initial conversation is busy loading.
514 if (disableOffscreenLoading) {
515 reason = LOAD_WAIT_UNTIL_VISIBLE;
516 LogUtils.i(LOG_TAG, "SHOWCONV: CVF waiting until visible to load (%s)", this);
517 } else if (getListController().isInitialConversationLoading()) {
518 reason = LOAD_WAIT_FOR_INITIAL_CONVERSATION;
519 LogUtils.i(LOG_TAG, "SHOWCONV: CVF waiting for initial to finish (%s)", this);
520 getListController().registerConversationLoadedObserver(mLoadedObserver);
521 } else {
522 LogUtils.i(LOG_TAG,
523 "SHOWCONV: CVF is not visible, but no reason to wait. loading now. (%s)",
524 this);
525 reason = LOAD_NOW;
526 }
Andy Huang632721e2012-04-11 16:57:26 -0700527 }
Andy Huang9d3fd922012-09-26 22:23:58 -0700528
529 mLoadWaitReason = reason;
530 if (mLoadWaitReason == LOAD_NOW) {
531 startConversationLoad();
532 }
533 }
534
535 private void handleDelayedConversationLoad() {
536 resetLoadWaiting();
537 startConversationLoad();
538 }
539
540 private void startConversationLoad() {
mindyp3bcf1802012-09-09 11:17:00 -0700541 mWebView.setVisibility(View.VISIBLE);
mindypf4fce122012-09-14 15:55:33 -0700542 getLoaderManager().initLoader(MESSAGE_LOADER, Bundle.EMPTY, getMessageLoaderCallbacks());
mindyp3bcf1802012-09-09 11:17:00 -0700543 // TODO(mindyp): don't show loading status for a previously rendered
544 // conversation. Ielieve this is better done by making sure don't show loading status
545 // until XX ms have passed without loading completed.
546 showLoadingStatus();
Mindy Pereira8e915722012-02-16 14:42:56 -0800547 }
548
Andy Huang7d4746e2012-10-17 17:03:17 -0700549 private void revealConversation() {
Andy Huang243c2362013-03-01 17:50:35 -0800550 timerMark("revealing conversation");
Andy Huang30bcfe72012-10-18 18:09:03 -0700551 dismissLoadingStatus(mOnProgressDismiss);
Andy Huang7d4746e2012-10-17 17:03:17 -0700552 }
553
Andy Huang9d3fd922012-09-26 22:23:58 -0700554 private boolean isLoadWaiting() {
555 return mLoadWaitReason != LOAD_NOW;
556 }
557
Andy Huang51067132012-03-12 20:08:19 -0700558 private void renderConversation(MessageCursor messageCursor) {
mindyp3bcf1802012-09-09 11:17:00 -0700559 final String convHtml = renderMessageBodies(messageCursor, mEnableContentReadySignal);
Andy Huang243c2362013-03-01 17:50:35 -0800560 timerMark("rendered conversation");
Andy Huangbd544e32012-05-29 15:56:51 -0700561
562 if (DEBUG_DUMP_CONVERSATION_HTML) {
563 java.io.FileWriter fw = null;
564 try {
565 fw = new java.io.FileWriter("/sdcard/conv" + mConversation.id
566 + ".html");
567 fw.write(convHtml);
568 } catch (java.io.IOException e) {
569 e.printStackTrace();
570 } finally {
571 if (fw != null) {
572 try {
573 fw.close();
574 } catch (java.io.IOException e) {
575 e.printStackTrace();
576 }
577 }
578 }
579 }
580
Andy Huange964eee2012-10-02 19:24:58 -0700581 // save off existing scroll position before re-rendering
582 if (mWebViewLoadedData) {
583 mWebViewYPercent = calculateScrollYPercent();
584 }
585
Andy Huangbd544e32012-05-29 15:56:51 -0700586 mWebView.loadDataWithBaseURL(mBaseUri, convHtml, "text/html", "utf-8", null);
Andy Huange964eee2012-10-02 19:24:58 -0700587 mWebViewLoadedData = true;
Andy Huang63b3c672012-10-05 19:27:28 -0700588 mWebViewLoadStartMs = SystemClock.uptimeMillis();
Andy Huang51067132012-03-12 20:08:19 -0700589 }
590
Andy Huang7bdc3752012-03-25 17:18:19 -0700591 /**
592 * Populate the adapter with overlay views (message headers, super-collapsed blocks, a
593 * conversation header), and return an HTML document with spacer divs inserted for all overlays.
594 *
595 */
mindyp3bcf1802012-09-09 11:17:00 -0700596 private String renderMessageBodies(MessageCursor messageCursor,
597 boolean enableContentReadySignal) {
Andy Huangf70fc402012-02-17 15:37:42 -0800598 int pos = -1;
Andy Huang632721e2012-04-11 16:57:26 -0700599
Andy Huang1ee96b22012-08-24 20:19:53 -0700600 LogUtils.d(LOG_TAG, "IN renderMessageBodies, fragment=%s", this);
Andy Huang7bdc3752012-03-25 17:18:19 -0700601 boolean allowNetworkImages = false;
602
Andy Huangc7543572012-04-03 15:34:29 -0700603 // TODO: re-use any existing adapter item state (expanded, details expanded, show pics)
Andy Huang28b7aee2012-08-20 20:27:32 -0700604
Andy Huang7bdc3752012-03-25 17:18:19 -0700605 // Walk through the cursor and build up an overlay adapter as you go.
606 // Each overlay has an entry in the adapter for easy scroll handling in the container.
607 // Items are not necessarily 1:1 in cursor and adapter because of super-collapsed blocks.
608 // When adding adapter items, also add their heights to help the container later determine
609 // overlay dimensions.
610
Andy Huangdb620fe2012-08-24 15:45:28 -0700611 // When re-rendering, prevent ConversationContainer from laying out overlays until after
612 // the new spacers are positioned by WebView.
613 mConversationContainer.invalidateSpacerGeometry();
614
Andy Huang7bdc3752012-03-25 17:18:19 -0700615 mAdapter.clear();
616
Andy Huang47aa9c92012-07-31 15:37:21 -0700617 // re-evaluate the message parts of the view state, since the messages may have changed
618 // since the previous render
619 final ConversationViewState prevState = mViewState;
620 mViewState = new ConversationViewState(prevState);
621
Andy Huang5ff63742012-03-16 20:30:23 -0700622 // N.B. the units of height for spacers are actually dp and not px because WebView assumes
Andy Huang2e9acfe2012-03-15 22:39:36 -0700623 // a pixel is an mdpi pixel, unless you set device-dpi.
Andy Huang5ff63742012-03-16 20:30:23 -0700624
Andy Huang7bdc3752012-03-25 17:18:19 -0700625 // add a single conversation header item
626 final int convHeaderPos = mAdapter.addConversationHeader(mConversation);
Andy Huang23014702012-07-09 12:50:36 -0700627 final int convHeaderPx = measureOverlayHeight(convHeaderPos);
Andy Huang5ff63742012-03-16 20:30:23 -0700628
Andy Huang02f9d182012-11-28 22:38:02 -0800629 mTemplates.startConversation(mWebView.screenPxToWebPx(mSideMarginPx),
Andy Huang256b35c2012-08-22 15:19:13 -0700630 mWebView.screenPxToWebPx(convHeaderPx));
Andy Huang3233bff2012-03-20 19:38:45 -0700631
Andy Huang46dfba62012-04-19 01:47:32 -0700632 int collapsedStart = -1;
Andy Huang839ada22012-07-20 15:48:40 -0700633 ConversationMessage prevCollapsedMsg = null;
Andy Huang46dfba62012-04-19 01:47:32 -0700634 boolean prevSafeForImages = false;
635
Andy Huangf70fc402012-02-17 15:37:42 -0800636 while (messageCursor.moveToPosition(++pos)) {
Andy Huang839ada22012-07-20 15:48:40 -0700637 final ConversationMessage msg = messageCursor.getMessage();
Andy Huang46dfba62012-04-19 01:47:32 -0700638
Scott Kennedy20273842012-11-07 11:16:21 -0800639 final boolean safeForImages =
640 msg.alwaysShowImages || prevState.getShouldShowImages(msg);
Andy Huang3233bff2012-03-20 19:38:45 -0700641 allowNetworkImages |= safeForImages;
Andy Huang24055282012-03-27 17:37:06 -0700642
Paul Westbrook08098ec2012-08-12 15:30:28 -0700643 final Integer savedExpanded = prevState.getExpansionState(msg);
644 final int expandedState;
Andy Huang839ada22012-07-20 15:48:40 -0700645 if (savedExpanded != null) {
Andy Huang1ee96b22012-08-24 20:19:53 -0700646 if (ExpansionState.isSuperCollapsed(savedExpanded) && messageCursor.isLast()) {
647 // override saved state when this is now the new last message
648 // this happens to the second-to-last message when you discard a draft
649 expandedState = ExpansionState.EXPANDED;
650 } else {
651 expandedState = savedExpanded;
652 }
Andy Huang839ada22012-07-20 15:48:40 -0700653 } else {
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700654 // new messages that are not expanded default to being eligible for super-collapse
Paul Westbrook08098ec2012-08-12 15:30:28 -0700655 expandedState = (!msg.read || msg.starred || messageCursor.isLast()) ?
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700656 ExpansionState.EXPANDED : ExpansionState.SUPER_COLLAPSED;
Andy Huang839ada22012-07-20 15:48:40 -0700657 }
Scott Kennedy20273842012-11-07 11:16:21 -0800658 mViewState.setShouldShowImages(msg, prevState.getShouldShowImages(msg));
Paul Westbrook08098ec2012-08-12 15:30:28 -0700659 mViewState.setExpansionState(msg, expandedState);
Andy Huangc7543572012-04-03 15:34:29 -0700660
Andy Huang839ada22012-07-20 15:48:40 -0700661 // save off "read" state from the cursor
662 // later, the view may not match the cursor (e.g. conversation marked read on open)
Andy Huang423bea22012-08-21 12:00:49 -0700663 // however, if a previous state indicated this message was unread, trust that instead
664 // so "mark unread" marks all originally unread messages
665 mViewState.setReadState(msg, msg.read && !prevState.isUnread(msg));
Andy Huang839ada22012-07-20 15:48:40 -0700666
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700667 // We only want to consider this for inclusion in the super collapsed block if
668 // 1) The we don't have previous state about this message (The first time that the
669 // user opens a conversation)
670 // 2) The previously saved state for this message indicates that this message is
671 // in the super collapsed block.
672 if (ExpansionState.isSuperCollapsed(expandedState)) {
673 // contribute to a super-collapsed block that will be emitted just before the
674 // next expanded header
675 if (collapsedStart < 0) {
676 collapsedStart = pos;
Andy Huang46dfba62012-04-19 01:47:32 -0700677 }
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700678 prevCollapsedMsg = msg;
679 prevSafeForImages = safeForImages;
Andrew Sapperstein7dc7fa02013-05-08 16:39:31 -0700680
681 // This line puts the from address in the address cache so that
682 // we get the sender image for it if it's in a super-collapsed block.
683 getAddress(msg.getFrom());
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700684 continue;
Andy Huang46dfba62012-04-19 01:47:32 -0700685 }
Andy Huang24055282012-03-27 17:37:06 -0700686
Andy Huang46dfba62012-04-19 01:47:32 -0700687 // resolve any deferred decisions on previous collapsed items
688 if (collapsedStart >= 0) {
689 if (pos - collapsedStart == 1) {
690 // special-case for a single collapsed message: no need to super-collapse it
691 renderMessage(prevCollapsedMsg, false /* expanded */,
692 prevSafeForImages);
693 } else {
694 renderSuperCollapsedBlock(collapsedStart, pos - 1);
695 }
696 prevCollapsedMsg = null;
697 collapsedStart = -1;
698 }
Andy Huang7bdc3752012-03-25 17:18:19 -0700699
Paul Westbrook08098ec2012-08-12 15:30:28 -0700700 renderMessage(msg, ExpansionState.isExpanded(expandedState), safeForImages);
Mindy Pereira9b875682012-02-15 18:10:54 -0800701 }
Andy Huang3233bff2012-03-20 19:38:45 -0700702
703 mWebView.getSettings().setBlockNetworkImage(!allowNetworkImages);
704
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700705 final boolean applyTransforms = shouldApplyTransforms();
Andy Huang57f354c2013-04-11 17:23:40 -0700706
Andy Huang0180f272013-03-08 18:33:22 -0800707 final MailPrefs prefs = MailPrefs.get(getContext());
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700708
Andy Huangc1fb9a92013-02-11 13:09:12 -0800709 // If the conversation has specified a base uri, use it here, otherwise use mBaseUri
710 return mTemplates.endConversation(mBaseUri, mConversation.getBaseUri(mBaseUri), 320,
Andy Huang5ea5a832013-03-07 16:49:09 -0800711 mWebView.getViewportWidth(), enableContentReadySignal, isOverviewMode(mAccount),
Andy Huang57f354c2013-04-11 17:23:40 -0700712 applyTransforms, applyTransforms);
Mindy Pereira9b875682012-02-15 18:10:54 -0800713 }
Mindy Pereira674afa42012-02-17 14:05:24 -0800714
Andy Huang46dfba62012-04-19 01:47:32 -0700715 private void renderSuperCollapsedBlock(int start, int end) {
716 final int blockPos = mAdapter.addSuperCollapsedBlock(start, end);
Andy Huang23014702012-07-09 12:50:36 -0700717 final int blockPx = measureOverlayHeight(blockPos);
718 mTemplates.appendSuperCollapsedHtml(start, mWebView.screenPxToWebPx(blockPx));
Andy Huang46dfba62012-04-19 01:47:32 -0700719 }
720
Andy Huang839ada22012-07-20 15:48:40 -0700721 private void renderMessage(ConversationMessage msg, boolean expanded,
722 boolean safeForImages) {
Scott Kennedy20273842012-11-07 11:16:21 -0800723 final int headerPos = mAdapter.addMessageHeader(msg, expanded,
724 mViewState.getShouldShowImages(msg));
Andy Huang46dfba62012-04-19 01:47:32 -0700725 final MessageHeaderItem headerItem = (MessageHeaderItem) mAdapter.getItem(headerPos);
726
727 final int footerPos = mAdapter.addMessageFooter(headerItem);
728
729 // Measure item header and footer heights to allocate spacers in HTML
730 // But since the views themselves don't exist yet, render each item temporarily into
731 // a host view for measurement.
Andy Huang23014702012-07-09 12:50:36 -0700732 final int headerPx = measureOverlayHeight(headerPos);
733 final int footerPx = measureOverlayHeight(footerPos);
Andy Huang46dfba62012-04-19 01:47:32 -0700734
Andy Huang256b35c2012-08-22 15:19:13 -0700735 mTemplates.appendMessageHtml(msg, expanded, safeForImages,
Andy Huang23014702012-07-09 12:50:36 -0700736 mWebView.screenPxToWebPx(headerPx), mWebView.screenPxToWebPx(footerPx));
Andy Huang243c2362013-03-01 17:50:35 -0800737 timerMark("rendered message");
Andy Huang46dfba62012-04-19 01:47:32 -0700738 }
739
740 private String renderCollapsedHeaders(MessageCursor cursor,
741 SuperCollapsedBlockItem blockToReplace) {
742 final List<ConversationOverlayItem> replacements = Lists.newArrayList();
743
744 mTemplates.reset();
745
Mark Wei2b24e992012-09-10 16:40:07 -0700746 // In devices with non-integral density multiplier, screen pixels translate to non-integral
747 // web pixels. Keep track of the error that occurs when we cast all heights to int
748 float error = 0f;
Andy Huang46dfba62012-04-19 01:47:32 -0700749 for (int i = blockToReplace.getStart(), end = blockToReplace.getEnd(); i <= end; i++) {
750 cursor.moveToPosition(i);
Andy Huang839ada22012-07-20 15:48:40 -0700751 final ConversationMessage msg = cursor.getMessage();
Andy Huang46dfba62012-04-19 01:47:32 -0700752 final MessageHeaderItem header = mAdapter.newMessageHeaderItem(msg,
Scott Kennedy20273842012-11-07 11:16:21 -0800753 false /* expanded */, mViewState.getShouldShowImages(msg));
Andy Huang46dfba62012-04-19 01:47:32 -0700754 final MessageFooterItem footer = mAdapter.newMessageFooterItem(header);
755
Andy Huang23014702012-07-09 12:50:36 -0700756 final int headerPx = measureOverlayHeight(header);
757 final int footerPx = measureOverlayHeight(footer);
Mark Wei2b24e992012-09-10 16:40:07 -0700758 error += mWebView.screenPxToWebPxError(headerPx)
759 + mWebView.screenPxToWebPxError(footerPx);
760
761 // When the error becomes greater than 1 pixel, make the next header 1 pixel taller
762 int correction = 0;
763 if (error >= 1) {
764 correction = 1;
765 error -= 1;
766 }
Andy Huang46dfba62012-04-19 01:47:32 -0700767
Andy Huang256b35c2012-08-22 15:19:13 -0700768 mTemplates.appendMessageHtml(msg, false /* expanded */, msg.alwaysShowImages,
Mark Wei2b24e992012-09-10 16:40:07 -0700769 mWebView.screenPxToWebPx(headerPx) + correction,
770 mWebView.screenPxToWebPx(footerPx));
Andy Huang46dfba62012-04-19 01:47:32 -0700771 replacements.add(header);
772 replacements.add(footer);
Andy Huang839ada22012-07-20 15:48:40 -0700773
Paul Westbrook08098ec2012-08-12 15:30:28 -0700774 mViewState.setExpansionState(msg, ExpansionState.COLLAPSED);
Andy Huang46dfba62012-04-19 01:47:32 -0700775 }
776
777 mAdapter.replaceSuperCollapsedBlock(blockToReplace, replacements);
Andy Huang06c03622012-10-22 18:59:45 -0700778 mAdapter.notifyDataSetChanged();
Andy Huang46dfba62012-04-19 01:47:32 -0700779
780 return mTemplates.emit();
781 }
782
783 private int measureOverlayHeight(int position) {
784 return measureOverlayHeight(mAdapter.getItem(position));
785 }
786
Andy Huang7bdc3752012-03-25 17:18:19 -0700787 /**
Andy Huangb8331b42012-07-16 19:08:53 -0700788 * Measure the height of an adapter view by rendering an adapter item into a temporary
Andy Huang46dfba62012-04-19 01:47:32 -0700789 * host view, and asking the view to immediately measure itself. This method will reuse
Andy Huang7bdc3752012-03-25 17:18:19 -0700790 * a previous adapter view from {@link ConversationContainer}'s scrap views if one was generated
791 * earlier.
792 * <p>
Andy Huang46dfba62012-04-19 01:47:32 -0700793 * After measuring the height, this method also saves the height in the
794 * {@link ConversationOverlayItem} for later use in overlay positioning.
Andy Huang7bdc3752012-03-25 17:18:19 -0700795 *
Andy Huang46dfba62012-04-19 01:47:32 -0700796 * @param convItem adapter item with data to render and measure
Andy Huang23014702012-07-09 12:50:36 -0700797 * @return height of the rendered view in screen px
Andy Huang7bdc3752012-03-25 17:18:19 -0700798 */
Andy Huang46dfba62012-04-19 01:47:32 -0700799 private int measureOverlayHeight(ConversationOverlayItem convItem) {
Andy Huang7bdc3752012-03-25 17:18:19 -0700800 final int type = convItem.getType();
801
802 final View convertView = mConversationContainer.getScrapView(type);
Andy Huangb8331b42012-07-16 19:08:53 -0700803 final View hostView = mAdapter.getView(convItem, convertView, mConversationContainer,
804 true /* measureOnly */);
Andy Huang7bdc3752012-03-25 17:18:19 -0700805 if (convertView == null) {
806 mConversationContainer.addScrapView(type, hostView);
807 }
808
Andy Huang9875bb42012-04-04 20:36:21 -0700809 final int heightPx = mConversationContainer.measureOverlay(hostView);
Andy Huang7bdc3752012-03-25 17:18:19 -0700810 convItem.setHeight(heightPx);
Andy Huang9875bb42012-04-04 20:36:21 -0700811 convItem.markMeasurementValid();
Andy Huang7bdc3752012-03-25 17:18:19 -0700812
Andy Huang23014702012-07-09 12:50:36 -0700813 return heightPx;
Andy Huang7bdc3752012-03-25 17:18:19 -0700814 }
815
Andy Huang5ff63742012-03-16 20:30:23 -0700816 @Override
817 public void onConversationViewHeaderHeightChange(int newHeight) {
Mark Weiab2d9982012-09-25 13:06:17 -0700818 final int h = mWebView.screenPxToWebPx(newHeight);
819
820 mWebView.loadUrl(String.format("javascript:setConversationHeaderSpacerHeight(%s);", h));
Andy Huang5ff63742012-03-16 20:30:23 -0700821 }
822
Andy Huang3233bff2012-03-20 19:38:45 -0700823 // END conversation header callbacks
824
825 // START message header callbacks
826 @Override
Andy Huangc7543572012-04-03 15:34:29 -0700827 public void setMessageSpacerHeight(MessageHeaderItem item, int newSpacerHeightPx) {
828 mConversationContainer.invalidateSpacerGeometry();
829
830 // update message HTML spacer height
Andy Huang23014702012-07-09 12:50:36 -0700831 final int h = mWebView.screenPxToWebPx(newSpacerHeightPx);
832 LogUtils.i(LAYOUT_TAG, "setting HTML spacer h=%dwebPx (%dscreenPx)", h,
833 newSpacerHeightPx);
Vikram Aggarwal5349ce12012-09-24 14:12:40 -0700834 mWebView.loadUrl(String.format("javascript:setMessageHeaderSpacerHeight('%s', %s);",
Andy Huang014ea4c2012-09-25 14:50:54 -0700835 mTemplates.getMessageDomId(item.getMessage()), h));
Andy Huang3233bff2012-03-20 19:38:45 -0700836 }
837
838 @Override
Andy Huangc7543572012-04-03 15:34:29 -0700839 public void setMessageExpanded(MessageHeaderItem item, int newSpacerHeightPx) {
840 mConversationContainer.invalidateSpacerGeometry();
841
842 // show/hide the HTML message body and update the spacer height
Andy Huang23014702012-07-09 12:50:36 -0700843 final int h = mWebView.screenPxToWebPx(newSpacerHeightPx);
844 LogUtils.i(LAYOUT_TAG, "setting HTML spacer expanded=%s h=%dwebPx (%dscreenPx)",
845 item.isExpanded(), h, newSpacerHeightPx);
Vikram Aggarwal5349ce12012-09-24 14:12:40 -0700846 mWebView.loadUrl(String.format("javascript:setMessageBodyVisible('%s', %s, %s);",
Andy Huang014ea4c2012-09-25 14:50:54 -0700847 mTemplates.getMessageDomId(item.getMessage()), item.isExpanded(), h));
Andy Huang839ada22012-07-20 15:48:40 -0700848
Andy Huang014ea4c2012-09-25 14:50:54 -0700849 mViewState.setExpansionState(item.getMessage(),
Paul Westbrook08098ec2012-08-12 15:30:28 -0700850 item.isExpanded() ? ExpansionState.EXPANDED : ExpansionState.COLLAPSED);
Andy Huang3233bff2012-03-20 19:38:45 -0700851 }
852
853 @Override
Scott Kennedyeb9a4bd2012-11-12 10:33:04 -0800854 public void showExternalResources(final Message msg) {
Scott Kennedy20273842012-11-07 11:16:21 -0800855 mViewState.setShouldShowImages(msg, true);
Andy Huang3233bff2012-03-20 19:38:45 -0700856 mWebView.getSettings().setBlockNetworkImage(false);
Scott Kennedyeb9a4bd2012-11-12 10:33:04 -0800857 mWebView.loadUrl("javascript:unblockImages(['" + mTemplates.getMessageDomId(msg) + "']);");
858 }
859
860 @Override
861 public void showExternalResources(final String senderRawAddress) {
862 mWebView.getSettings().setBlockNetworkImage(false);
863
864 final Address sender = getAddress(senderRawAddress);
865 final MessageCursor cursor = getMessageCursor();
866
867 final List<String> messageDomIds = new ArrayList<String>();
868
869 int pos = -1;
870 while (cursor.moveToPosition(++pos)) {
871 final ConversationMessage message = cursor.getMessage();
872 if (sender.equals(getAddress(message.getFrom()))) {
873 message.alwaysShowImages = true;
874
875 mViewState.setShouldShowImages(message, true);
876 messageDomIds.add(mTemplates.getMessageDomId(message));
877 }
878 }
879
880 final String url = String.format(
881 "javascript:unblockImages(['%s']);", TextUtils.join("','", messageDomIds));
882 mWebView.loadUrl(url);
Andy Huang3233bff2012-03-20 19:38:45 -0700883 }
Alice Yang1ebc2db2013-03-14 21:21:44 -0700884
885 @Override
Andy Huang75b52a52013-03-15 15:40:24 -0700886 public boolean supportsMessageTransforms() {
887 return true;
888 }
889
890 @Override
Alice Yang1ebc2db2013-03-14 21:21:44 -0700891 public String getMessageTransforms(final Message msg) {
892 final String domId = mTemplates.getMessageDomId(msg);
893 return (domId == null) ? null : mMessageTransforms.get(domId);
894 }
895
Andy Huang3233bff2012-03-20 19:38:45 -0700896 // END message header callbacks
Andy Huang5ff63742012-03-16 20:30:23 -0700897
Andy Huang46dfba62012-04-19 01:47:32 -0700898 @Override
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700899 public void showUntransformedConversation() {
900 super.showUntransformedConversation();
901 renderConversation(getMessageCursor());
902 }
903
904 @Override
Andy Huang46dfba62012-04-19 01:47:32 -0700905 public void onSuperCollapsedClick(SuperCollapsedBlockItem item) {
mindypf4fce122012-09-14 15:55:33 -0700906 MessageCursor cursor = getMessageCursor();
907 if (cursor == null || !mViewsCreated) {
Andy Huang46dfba62012-04-19 01:47:32 -0700908 return;
909 }
910
mindypf4fce122012-09-14 15:55:33 -0700911 mTempBodiesHtml = renderCollapsedHeaders(cursor, item);
Andy Huang46dfba62012-04-19 01:47:32 -0700912 mWebView.loadUrl("javascript:replaceSuperCollapsedBlock(" + item.getStart() + ")");
913 }
914
Andy Huang47aa9c92012-07-31 15:37:21 -0700915 private void showNewMessageNotification(NewMessagesInfo info) {
916 final TextView descriptionView = (TextView) mNewMessageBar.findViewById(
917 R.id.new_message_description);
918 descriptionView.setText(info.getNotificationText());
919 mNewMessageBar.setVisibility(View.VISIBLE);
920 }
921
922 private void onNewMessageBarClick() {
923 mNewMessageBar.setVisibility(View.GONE);
924
mindypf4fce122012-09-14 15:55:33 -0700925 renderConversation(getMessageCursor()); // mCursor is already up-to-date
926 // per onLoadFinished()
Andy Huang5fbda022012-02-28 18:22:03 -0800927 }
928
Andy Huangadbf3e82012-10-13 13:30:19 -0700929 private static OverlayPosition[] parsePositions(final String[] topArray,
930 final String[] bottomArray) {
931 final int len = topArray.length;
932 final OverlayPosition[] positions = new OverlayPosition[len];
Andy Huangb5078b22012-03-05 19:52:29 -0800933 for (int i = 0; i < len; i++) {
Andy Huangadbf3e82012-10-13 13:30:19 -0700934 positions[i] = new OverlayPosition(
935 Integer.parseInt(topArray[i]), Integer.parseInt(bottomArray[i]));
Andy Huangb5078b22012-03-05 19:52:29 -0800936 }
Andy Huangadbf3e82012-10-13 13:30:19 -0700937 return positions;
Andy Huangb5078b22012-03-05 19:52:29 -0800938 }
939
Andy Huang16174812012-08-16 16:40:35 -0700940 private Address getAddress(String rawFrom) {
Andy Huang543e7092013-04-22 11:44:56 -0700941 Address addr;
942 synchronized (mAddressCache) {
943 addr = mAddressCache.get(rawFrom);
944 if (addr == null) {
945 addr = Address.getEmailAddress(rawFrom);
946 mAddressCache.put(rawFrom, addr);
947 }
Andy Huang16174812012-08-16 16:40:35 -0700948 }
949 return addr;
950 }
951
Andy Huang9d3fd922012-09-26 22:23:58 -0700952 private void ensureContentSizeChangeListener() {
953 if (mWebViewSizeChangeListener == null) {
Andy Huangc1fb9a92013-02-11 13:09:12 -0800954 mWebViewSizeChangeListener = new ContentSizeChangeListener() {
Andy Huang9d3fd922012-09-26 22:23:58 -0700955 @Override
956 public void onHeightChange(int h) {
957 // When WebKit says the DOM height has changed, re-measure
958 // bodies and re-position their headers.
959 // This is separate from the typical JavaScript DOM change
960 // listeners because cases like NARROW_COLUMNS text reflow do not trigger DOM
961 // events.
962 mWebView.loadUrl("javascript:measurePositions();");
963 }
964 };
965 }
966 mWebView.setContentSizeChangeListener(mWebViewSizeChangeListener);
967 }
968
Andy Huangadbf3e82012-10-13 13:30:19 -0700969 private static boolean isOverviewMode(Account acct) {
Andy Huangccf67802013-03-15 14:31:57 -0700970 return acct.settings.isOverviewMode();
Andy Huangadbf3e82012-10-13 13:30:19 -0700971 }
972
973 private void setupOverviewMode() {
Andy Huang02f9d182012-11-28 22:38:02 -0800974 // for now, overview mode means use the built-in WebView zoom and disable custom scale
975 // gesture handling
Andy Huangadbf3e82012-10-13 13:30:19 -0700976 final boolean overviewMode = isOverviewMode(mAccount);
977 final WebSettings settings = mWebView.getSettings();
Andy Huang06def562012-10-14 00:19:11 -0700978 settings.setUseWideViewPort(overviewMode);
Andy Huangc1fb9a92013-02-11 13:09:12 -0800979
980 final OnScaleGestureListener listener;
981
Andy Huang57f354c2013-04-11 17:23:40 -0700982 settings.setSupportZoom(overviewMode);
983 settings.setBuiltInZoomControls(overviewMode);
984 if (overviewMode) {
985 settings.setDisplayZoomControls(false);
Andy Huangadbf3e82012-10-13 13:30:19 -0700986 }
Andy Huang57f354c2013-04-11 17:23:40 -0700987 listener = ENABLE_CSS_ZOOM && !overviewMode ? new CssScaleInterceptor() : null;
Andy Huangc1fb9a92013-02-11 13:09:12 -0800988
Andy Huang57f354c2013-04-11 17:23:40 -0700989 mWebView.setOnScaleGestureListener(listener);
Andy Huangadbf3e82012-10-13 13:30:19 -0700990 }
991
Paul Westbrook542fec92012-09-18 14:47:51 -0700992 private class ConversationWebViewClient extends AbstractConversationWebViewClient {
Andy Huang17a9cde2012-03-09 18:03:16 -0800993 @Override
994 public void onPageFinished(WebView view, String url) {
Andy Huang9a8bc1e2012-10-23 19:48:25 -0700995 // Ignore unsafe calls made after a fragment is detached from an activity.
996 // This method needs to, for example, get at the loader manager, which needs
997 // the fragment to be added.
Andy Huang9a8bc1e2012-10-23 19:48:25 -0700998 if (!isAdded() || !mViewsCreated) {
Andy Huangde56e972012-07-26 18:23:08 -0700999 LogUtils.i(LOG_TAG, "ignoring CVF.onPageFinished, url=%s fragment=%s", url,
Andy Huangb95da852012-07-18 14:16:58 -07001000 ConversationViewFragment.this);
1001 return;
1002 }
1003
Andy Huang30bcfe72012-10-18 18:09:03 -07001004 LogUtils.i(LOG_TAG, "IN CVF.onPageFinished, url=%s fragment=%s wv=%s t=%sms", url,
1005 ConversationViewFragment.this, view,
Andy Huang63b3c672012-10-05 19:27:28 -07001006 (SystemClock.uptimeMillis() - mWebViewLoadStartMs));
Andy Huang632721e2012-04-11 16:57:26 -07001007
Andy Huang9d3fd922012-09-26 22:23:58 -07001008 ensureContentSizeChangeListener();
1009
mindyp3bcf1802012-09-09 11:17:00 -07001010 if (!mEnableContentReadySignal) {
Andy Huang7d4746e2012-10-17 17:03:17 -07001011 revealConversation();
mindyp3bcf1802012-09-09 11:17:00 -07001012 }
Andy Huang9d3fd922012-09-26 22:23:58 -07001013
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001014 final Set<String> emailAddresses = Sets.newHashSet();
Andy Huang543e7092013-04-22 11:44:56 -07001015 final List<Address> cacheCopy;
1016 synchronized (mAddressCache) {
1017 cacheCopy = ImmutableList.copyOf(mAddressCache.values());
1018 }
1019 for (Address addr : cacheCopy) {
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001020 emailAddresses.add(addr.getAddress());
Andy Huangb8331b42012-07-16 19:08:53 -07001021 }
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001022 ContactLoaderCallbacks callbacks = getContactInfoSource();
1023 getContactInfoSource().setSenders(emailAddresses);
1024 getLoaderManager().restartLoader(CONTACT_LOADER, Bundle.EMPTY, callbacks);
Andy Huang17a9cde2012-03-09 18:03:16 -08001025 }
1026
Andy Huangaf5d4e02012-03-19 19:02:12 -07001027 @Override
1028 public boolean shouldOverrideUrlLoading(WebView view, String url) {
Paul Westbrook542fec92012-09-18 14:47:51 -07001029 return mViewsCreated && super.shouldOverrideUrlLoading(view, url);
Andy Huangaf5d4e02012-03-19 19:02:12 -07001030 }
Andy Huang17a9cde2012-03-09 18:03:16 -08001031 }
1032
Andy Huangf70fc402012-02-17 15:37:42 -08001033 /**
1034 * NOTE: all public methods must be listed in the proguard flags so that they can be accessed
1035 * via reflection and not stripped.
1036 *
1037 */
1038 private class MailJsBridge {
1039
1040 @SuppressWarnings("unused")
Mindy Pereira974c9662012-09-14 10:02:08 -07001041 @JavascriptInterface
Andy Huangadbf3e82012-10-13 13:30:19 -07001042 public void onWebContentGeometryChange(final String[] overlayTopStrs,
1043 final String[] overlayBottomStrs) {
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001044 getHandler().post(new FragmentRunnable("onWebContentGeometryChange") {
mindyp1b3cc472012-09-27 11:32:59 -07001045
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001046 @Override
1047 public void go() {
1048 try {
Andy Huang46dfba62012-04-19 01:47:32 -07001049 if (!mViewsCreated) {
mindyp1b3cc472012-09-27 11:32:59 -07001050 LogUtils.d(LOG_TAG, "ignoring webContentGeometryChange because views"
1051 + " are gone, %s", ConversationViewFragment.this);
Andy Huang46dfba62012-04-19 01:47:32 -07001052 return;
1053 }
Andy Huangadbf3e82012-10-13 13:30:19 -07001054 mConversationContainer.onGeometryChange(
1055 parsePositions(overlayTopStrs, overlayBottomStrs));
mindyp1b3cc472012-09-27 11:32:59 -07001056 if (mDiff != 0) {
1057 // SCROLL!
1058 int scale = (int) (mWebView.getScale() / mWebView.getInitialScale());
1059 if (scale > 1) {
1060 mWebView.scrollBy(0, (mDiff * (scale - 1)));
1061 }
1062 mDiff = 0;
1063 }
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001064 } catch (Throwable t) {
1065 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onWebContentGeometryChange");
Andy Huang46dfba62012-04-19 01:47:32 -07001066 }
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001067 }
1068 });
Andy Huang46dfba62012-04-19 01:47:32 -07001069 }
1070
1071 @SuppressWarnings("unused")
Mindy Pereira974c9662012-09-14 10:02:08 -07001072 @JavascriptInterface
Andy Huang46dfba62012-04-19 01:47:32 -07001073 public String getTempMessageBodies() {
1074 try {
1075 if (!mViewsCreated) {
1076 return "";
Andy Huangf70fc402012-02-17 15:37:42 -08001077 }
Andy Huang46dfba62012-04-19 01:47:32 -07001078
1079 final String s = mTempBodiesHtml;
1080 mTempBodiesHtml = null;
1081 return s;
1082 } catch (Throwable t) {
1083 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getTempMessageBodies");
1084 return "";
1085 }
Andy Huangf70fc402012-02-17 15:37:42 -08001086 }
1087
Andy Huang014ea4c2012-09-25 14:50:54 -07001088 @SuppressWarnings("unused")
1089 @JavascriptInterface
1090 public String getMessageBody(String domId) {
1091 try {
1092 final MessageCursor cursor = getMessageCursor();
1093 if (!mViewsCreated || cursor == null) {
1094 return "";
1095 }
1096
1097 int pos = -1;
1098 while (cursor.moveToPosition(++pos)) {
1099 final ConversationMessage msg = cursor.getMessage();
1100 if (TextUtils.equals(domId, mTemplates.getMessageDomId(msg))) {
1101 return msg.getBodyAsHtml();
1102 }
1103 }
1104
1105 return "";
1106
1107 } catch (Throwable t) {
1108 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getMessageBody");
1109 return "";
1110 }
1111 }
1112
mindyp3bcf1802012-09-09 11:17:00 -07001113 @SuppressWarnings("unused")
Mindy Pereira974c9662012-09-14 10:02:08 -07001114 @JavascriptInterface
Andy Huang543e7092013-04-22 11:44:56 -07001115 public String getMessageSender(String domId) {
1116 try {
1117 final MessageCursor cursor = getMessageCursor();
1118 if (!mViewsCreated || cursor == null) {
1119 return "";
1120 }
1121
1122 int pos = -1;
1123 while (cursor.moveToPosition(++pos)) {
1124 final ConversationMessage msg = cursor.getMessage();
1125 if (TextUtils.equals(domId, mTemplates.getMessageDomId(msg))) {
1126 return getAddress(msg.getFrom()).getAddress();
1127 }
1128 }
1129
1130 return "";
1131
1132 } catch (Throwable t) {
1133 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getMessageSender");
1134 return "";
1135 }
1136 }
1137
1138 @SuppressWarnings("unused")
1139 @JavascriptInterface
mindyp3bcf1802012-09-09 11:17:00 -07001140 public void onContentReady() {
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001141 getHandler().post(new FragmentRunnable("onContentReady") {
1142 @Override
1143 public void go() {
1144 try {
Andy Huang63b3c672012-10-05 19:27:28 -07001145 if (mWebViewLoadStartMs != 0) {
1146 LogUtils.i(LOG_TAG, "IN CVF.onContentReady, f=%s vis=%s t=%sms",
1147 ConversationViewFragment.this,
1148 isUserVisible(),
1149 (SystemClock.uptimeMillis() - mWebViewLoadStartMs));
1150 }
Andy Huang7d4746e2012-10-17 17:03:17 -07001151 revealConversation();
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001152 } catch (Throwable t) {
1153 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onContentReady");
1154 // Still try to show the conversation.
1155 revealConversation();
mindyp3bcf1802012-09-09 11:17:00 -07001156 }
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001157 }
1158 });
mindyp3bcf1802012-09-09 11:17:00 -07001159 }
Andy Huange964eee2012-10-02 19:24:58 -07001160
1161 @SuppressWarnings("unused")
1162 @JavascriptInterface
1163 public float getScrollYPercent() {
1164 try {
1165 return mWebViewYPercent;
1166 } catch (Throwable t) {
1167 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getScrollYPercent");
1168 return 0f;
1169 }
1170 }
Andy Huang05c70c82013-03-14 15:15:50 -07001171
1172 @SuppressWarnings("unused")
1173 @JavascriptInterface
1174 public void onMessageTransform(String messageDomId, String transformText) {
Andrew Sappersteinae92e152013-05-03 13:55:18 -07001175 try {
1176 LogUtils.i(LOG_TAG, "TRANSFORM: (%s) %s", messageDomId, transformText);
1177 mMessageTransforms.put(messageDomId, transformText);
1178 onConversationTransformed();
1179 } catch (Throwable t) {
1180 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onMessageTransform");
1181 return;
1182 }
Andy Huang05c70c82013-03-14 15:15:50 -07001183 }
Andy Huangf70fc402012-02-17 15:37:42 -08001184 }
1185
Andy Huang47aa9c92012-07-31 15:37:21 -07001186 private class NewMessagesInfo {
1187 int count;
Andy Huang06c03622012-10-22 18:59:45 -07001188 int countFromSelf;
Andy Huang47aa9c92012-07-31 15:37:21 -07001189 String senderAddress;
1190
1191 /**
1192 * Return the display text for the new message notification overlay. It will be formatted
1193 * appropriately for a single new message vs. multiple new messages.
1194 *
1195 * @return display text
1196 */
1197 public String getNotificationText() {
mindypad0c30d2012-09-25 12:09:13 -07001198 Resources res = getResources();
Andy Huang47aa9c92012-07-31 15:37:21 -07001199 if (count > 1) {
mindypad0c30d2012-09-25 12:09:13 -07001200 return res.getString(R.string.new_incoming_messages_many, count);
Andy Huang47aa9c92012-07-31 15:37:21 -07001201 } else {
Andy Huang16174812012-08-16 16:40:35 -07001202 final Address addr = getAddress(senderAddress);
mindypad0c30d2012-09-25 12:09:13 -07001203 return res.getString(R.string.new_incoming_messages_one,
1204 TextUtils.isEmpty(addr.getName()) ? addr.getAddress() : addr.getName());
Andy Huang47aa9c92012-07-31 15:37:21 -07001205 }
Andy Huang47aa9c92012-07-31 15:37:21 -07001206 }
1207 }
1208
mindypf4fce122012-09-14 15:55:33 -07001209 @Override
Paul Westbrookc42ad5e2013-05-09 16:52:15 -07001210 public void onMessageCursorLoadFinished(Loader<ObjectCursor<ConversationMessage>> loader,
1211 MessageCursor newCursor, MessageCursor oldCursor) {
mindypf4fce122012-09-14 15:55:33 -07001212 /*
1213 * what kind of changes affect the MessageCursor? 1. new message(s) 2.
1214 * read/unread state change 3. deleted message, either regular or draft
1215 * 4. updated message, either from self or from others, updated in
1216 * content or state or sender 5. star/unstar of message (technically
1217 * similar to #1) 6. other label change Use MessageCursor.hashCode() to
1218 * sort out interesting vs. no-op cursor updates.
1219 */
Andy Huangb8331b42012-07-16 19:08:53 -07001220
Andy Huang233d4352012-10-18 14:00:24 -07001221 if (oldCursor != null && !oldCursor.isClosed()) {
Andy Huang014ea4c2012-09-25 14:50:54 -07001222 final NewMessagesInfo info = getNewIncomingMessagesInfo(newCursor);
Andy Huangb8331b42012-07-16 19:08:53 -07001223
Andy Huang014ea4c2012-09-25 14:50:54 -07001224 if (info.count > 0) {
1225 // don't immediately render new incoming messages from other
1226 // senders
1227 // (to avoid a new message from losing the user's focus)
1228 LogUtils.i(LOG_TAG, "CONV RENDER: conversation updated"
Andy Huang9d3fd922012-09-26 22:23:58 -07001229 + ", holding cursor for new incoming message (%s)", this);
Andy Huang014ea4c2012-09-25 14:50:54 -07001230 showNewMessageNotification(info);
1231 return;
1232 }
1233
Andy Huang06c03622012-10-22 18:59:45 -07001234 final int oldState = oldCursor.getStateHashCode();
1235 final boolean changed = newCursor.getStateHashCode() != oldState;
Andy Huang233d4352012-10-18 14:00:24 -07001236
Andy Huang014ea4c2012-09-25 14:50:54 -07001237 if (!changed) {
1238 final boolean processedInPlace = processInPlaceUpdates(newCursor, oldCursor);
1239 if (processedInPlace) {
Andy Huang9d3fd922012-09-26 22:23:58 -07001240 LogUtils.i(LOG_TAG, "CONV RENDER: processed update(s) in place (%s)", this);
Andy Huang1ee96b22012-08-24 20:19:53 -07001241 } else {
mindypf4fce122012-09-14 15:55:33 -07001242 LogUtils.i(LOG_TAG, "CONV RENDER: uninteresting update"
Andy Huang9d3fd922012-09-26 22:23:58 -07001243 + ", ignoring this conversation update (%s)", this);
Andy Huang1ee96b22012-08-24 20:19:53 -07001244 }
Andy Huangb8331b42012-07-16 19:08:53 -07001245 return;
Andy Huang06c03622012-10-22 18:59:45 -07001246 } else if (info.countFromSelf == 1) {
1247 // Special-case the very common case of a new cursor that is the same as the old
1248 // one, except that there is a new message from yourself. This happens upon send.
1249 final boolean sameExceptNewLast = newCursor.getStateHashCode(1) == oldState;
1250 if (sameExceptNewLast) {
1251 LogUtils.i(LOG_TAG, "CONV RENDER: update is a single new message from self"
1252 + " (%s)", this);
1253 newCursor.moveToLast();
1254 processNewOutgoingMessage(newCursor.getMessage());
1255 return;
1256 }
Andy Huangb8331b42012-07-16 19:08:53 -07001257 }
Andy Huang6766b6e2012-09-28 12:43:52 -07001258 // cursors are different, and not due to an incoming message. fall
1259 // through and render.
1260 LogUtils.i(LOG_TAG, "CONV RENDER: conversation updated"
1261 + ", but not due to incoming message. rendering. (%s)", this);
Andy Huang06c03622012-10-22 18:59:45 -07001262
1263 if (DEBUG_DUMP_CURSOR_CONTENTS) {
1264 LogUtils.i(LOG_TAG, "old cursor: %s", oldCursor.getDebugDump());
1265 LogUtils.i(LOG_TAG, "new cursor: %s", newCursor.getDebugDump());
1266 }
Andy Huang6766b6e2012-09-28 12:43:52 -07001267 } else {
1268 LogUtils.i(LOG_TAG, "CONV RENDER: initial render. (%s)", this);
Andy Huang243c2362013-03-01 17:50:35 -08001269 timerMark("message cursor load finished");
Andy Huangb8331b42012-07-16 19:08:53 -07001270 }
1271
Mark Wei4071c2f2012-09-26 14:38:38 -07001272 // if layout hasn't happened, delay render
1273 // This is needed in addition to the showConversation() delay to speed
1274 // up rotation and restoration.
1275 if (mConversationContainer.getWidth() == 0) {
1276 mNeedRender = true;
1277 mConversationContainer.addOnLayoutChangeListener(this);
1278 } else {
1279 renderConversation(newCursor);
1280 }
Andy Huangb8331b42012-07-16 19:08:53 -07001281 }
1282
mindypf4fce122012-09-14 15:55:33 -07001283 private NewMessagesInfo getNewIncomingMessagesInfo(MessageCursor newCursor) {
1284 final NewMessagesInfo info = new NewMessagesInfo();
Andy Huangb8331b42012-07-16 19:08:53 -07001285
mindypf4fce122012-09-14 15:55:33 -07001286 int pos = -1;
1287 while (newCursor.moveToPosition(++pos)) {
1288 final Message m = newCursor.getMessage();
1289 if (!mViewState.contains(m)) {
1290 LogUtils.i(LOG_TAG, "conversation diff: found new msg: %s", m.uri);
Andy Huangb8331b42012-07-16 19:08:53 -07001291
Scott Kennedy8960f0a2012-11-07 15:35:50 -08001292 final Address from = getAddress(m.getFrom());
mindypf4fce122012-09-14 15:55:33 -07001293 // distinguish ours from theirs
1294 // new messages from the account owner should not trigger a
1295 // notification
1296 if (mAccount.ownsFromAddress(from.getAddress())) {
1297 LogUtils.i(LOG_TAG, "found message from self: %s", m.uri);
Andy Huang06c03622012-10-22 18:59:45 -07001298 info.countFromSelf++;
mindypf4fce122012-09-14 15:55:33 -07001299 continue;
1300 }
Andy Huangb8331b42012-07-16 19:08:53 -07001301
mindypf4fce122012-09-14 15:55:33 -07001302 info.count++;
Scott Kennedy8960f0a2012-11-07 15:35:50 -08001303 info.senderAddress = m.getFrom();
Andy Huangb8331b42012-07-16 19:08:53 -07001304 }
Andy Huangb8331b42012-07-16 19:08:53 -07001305 }
mindypf4fce122012-09-14 15:55:33 -07001306 return info;
Andy Huangb8331b42012-07-16 19:08:53 -07001307 }
1308
Andy Huang014ea4c2012-09-25 14:50:54 -07001309 private boolean processInPlaceUpdates(MessageCursor newCursor, MessageCursor oldCursor) {
1310 final Set<String> idsOfChangedBodies = Sets.newHashSet();
Andy Huang6b3d0d92012-10-30 15:46:48 -07001311 final List<Integer> changedOverlayPositions = Lists.newArrayList();
1312
Andy Huang014ea4c2012-09-25 14:50:54 -07001313 boolean changed = false;
1314
1315 int pos = 0;
1316 while (true) {
1317 if (!newCursor.moveToPosition(pos) || !oldCursor.moveToPosition(pos)) {
1318 break;
1319 }
1320
1321 final ConversationMessage newMsg = newCursor.getMessage();
1322 final ConversationMessage oldMsg = oldCursor.getMessage();
1323
Scott Kennedy8960f0a2012-11-07 15:35:50 -08001324 if (!TextUtils.equals(newMsg.getFrom(), oldMsg.getFrom()) ||
Andy Huang2a1e8e32012-10-23 18:54:57 -07001325 newMsg.isSending != oldMsg.isSending) {
Andy Huang6b3d0d92012-10-30 15:46:48 -07001326 mAdapter.updateItemsForMessage(newMsg, changedOverlayPositions);
Andy Huang2a1e8e32012-10-23 18:54:57 -07001327 LogUtils.i(LOG_TAG, "msg #%d (%d): detected from/sending change. isSending=%s",
1328 pos, newMsg.id, newMsg.isSending);
Andy Huang014ea4c2012-09-25 14:50:54 -07001329 }
1330
1331 // update changed message bodies in-place
1332 if (!TextUtils.equals(newMsg.bodyHtml, oldMsg.bodyHtml) ||
1333 !TextUtils.equals(newMsg.bodyText, oldMsg.bodyText)) {
1334 // maybe just set a flag to notify JS to re-request changed bodies
1335 idsOfChangedBodies.add('"' + mTemplates.getMessageDomId(newMsg) + '"');
1336 LogUtils.i(LOG_TAG, "msg #%d (%d): detected body change", pos, newMsg.id);
1337 }
1338
1339 pos++;
1340 }
1341
Andy Huang6b3d0d92012-10-30 15:46:48 -07001342
1343 if (!changedOverlayPositions.isEmpty()) {
Andy Huang06c03622012-10-22 18:59:45 -07001344 // notify once after the entire adapter is updated
Andy Huang6b3d0d92012-10-30 15:46:48 -07001345 mConversationContainer.onOverlayModelUpdate(changedOverlayPositions);
1346 changed = true;
Andy Huang06c03622012-10-22 18:59:45 -07001347 }
1348
Andy Huang014ea4c2012-09-25 14:50:54 -07001349 if (!idsOfChangedBodies.isEmpty()) {
1350 mWebView.loadUrl(String.format("javascript:replaceMessageBodies([%s]);",
1351 TextUtils.join(",", idsOfChangedBodies)));
1352 changed = true;
1353 }
1354
1355 return changed;
1356 }
1357
Andy Huang06c03622012-10-22 18:59:45 -07001358 private void processNewOutgoingMessage(ConversationMessage msg) {
1359 mTemplates.reset();
1360 // this method will add some items to mAdapter, but we deliberately want to avoid notifying
1361 // adapter listeners (i.e. ConversationContainer) until onWebContentGeometryChange is next
1362 // called, to prevent N+1 headers rendering with N message bodies.
1363 renderMessage(msg, true /* expanded */, msg.alwaysShowImages);
1364 mTempBodiesHtml = mTemplates.emit();
1365
1366 mViewState.setExpansionState(msg, ExpansionState.EXPANDED);
1367 // FIXME: should the provider set this as initial state?
1368 mViewState.setReadState(msg, false /* read */);
1369
Andy Huang91d782a2012-10-25 12:37:29 -07001370 // From now until the updated spacer geometry is returned, the adapter items are mismatched
1371 // with the existing spacers. Do not let them layout.
1372 mConversationContainer.invalidateSpacerGeometry();
1373
Andy Huang06c03622012-10-22 18:59:45 -07001374 mWebView.loadUrl("javascript:appendMessageHtml();");
1375 }
1376
Paul Westbrookcebcc642012-08-08 10:06:04 -07001377 private class SetCookieTask extends AsyncTask<Void, Void, Void> {
1378 final String mUri;
Paul Westbrookb8361c92012-09-27 10:57:14 -07001379 final Uri mAccountCookieQueryUri;
1380 final ContentResolver mResolver;
Paul Westbrookcebcc642012-08-08 10:06:04 -07001381
Paul Westbrookb8361c92012-09-27 10:57:14 -07001382 SetCookieTask(Context context, Uri baseUri, Uri accountCookieQueryUri) {
1383 mUri = baseUri.toString();
1384 mAccountCookieQueryUri = accountCookieQueryUri;
1385 mResolver = context.getContentResolver();
Paul Westbrookcebcc642012-08-08 10:06:04 -07001386 }
1387
1388 @Override
1389 public Void doInBackground(Void... args) {
Paul Westbrookb8361c92012-09-27 10:57:14 -07001390 // First query for the coookie string from the UI provider
1391 final Cursor cookieCursor = mResolver.query(mAccountCookieQueryUri,
1392 UIProvider.ACCOUNT_COOKIE_PROJECTION, null, null, null);
1393 if (cookieCursor == null) {
1394 return null;
1395 }
1396
1397 try {
1398 if (cookieCursor.moveToFirst()) {
1399 final String cookie = cookieCursor.getString(
1400 cookieCursor.getColumnIndex(UIProvider.AccountCookieColumns.COOKIE));
1401
1402 if (cookie != null) {
1403 final CookieSyncManager csm =
1404 CookieSyncManager.createInstance(getContext());
1405 CookieManager.getInstance().setCookie(mUri, cookie);
1406 csm.sync();
1407 }
1408 }
1409
1410 } finally {
1411 cookieCursor.close();
1412 }
1413
1414
Paul Westbrookcebcc642012-08-08 10:06:04 -07001415 return null;
1416 }
1417 }
mindyp36280f32012-09-09 16:11:23 -07001418
mindyp26d4d2d2012-09-18 17:30:32 -07001419 @Override
mindyp36280f32012-09-09 16:11:23 -07001420 public void onConversationUpdated(Conversation conv) {
1421 final ConversationViewHeader headerView = (ConversationViewHeader) mConversationContainer
1422 .findViewById(R.id.conversation_header);
mindypb2b98ba2012-09-24 14:13:58 -07001423 mConversation = conv;
mindyp9e0b2362012-09-09 16:31:21 -07001424 if (headerView != null) {
1425 headerView.onConversationUpdated(conv);
Vikram Aggarwal51ad9042013-01-17 13:18:09 -08001426 headerView.setSubject(conv.subject);
mindyp9e0b2362012-09-09 16:31:21 -07001427 }
mindyp36280f32012-09-09 16:11:23 -07001428 }
Mark Wei4071c2f2012-09-26 14:38:38 -07001429
1430 @Override
1431 public void onLayoutChange(View v, int left, int top, int right,
1432 int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
1433 boolean sizeChanged = mNeedRender
1434 && mConversationContainer.getWidth() != 0;
1435 if (sizeChanged) {
1436 mNeedRender = false;
1437 mConversationContainer.removeOnLayoutChangeListener(this);
1438 renderConversation(getMessageCursor());
1439 }
1440 }
mindyp1b3cc472012-09-27 11:32:59 -07001441
1442 @Override
1443 public void setMessageDetailsExpanded(MessageHeaderItem i, boolean expanded,
1444 int heightBefore) {
1445 mDiff = (expanded ? 1 : -1) * Math.abs(i.getHeight() - heightBefore);
1446 }
Andy Huang02f9d182012-11-28 22:38:02 -08001447
Andy Huangc1fb9a92013-02-11 13:09:12 -08001448 private class CssScaleInterceptor implements OnScaleGestureListener {
Andy Huang02f9d182012-11-28 22:38:02 -08001449
1450 private float getFocusXWebPx(ScaleGestureDetector detector) {
1451 return (detector.getFocusX() - mSideMarginPx) / mWebView.getInitialScale();
1452 }
1453
1454 private float getFocusYWebPx(ScaleGestureDetector detector) {
1455 return detector.getFocusY() / mWebView.getInitialScale();
1456 }
1457
1458 @Override
1459 public boolean onScale(ScaleGestureDetector detector) {
1460 mWebView.loadUrl(String.format("javascript:onScale(%s, %s, %s);",
1461 detector.getScaleFactor(), getFocusXWebPx(detector),
1462 getFocusYWebPx(detector)));
1463 return false;
1464 }
1465
1466 @Override
1467 public boolean onScaleBegin(ScaleGestureDetector detector) {
1468 mWebView.loadUrl(String.format("javascript:onScaleBegin(%s, %s);",
1469 getFocusXWebPx(detector), getFocusYWebPx(detector)));
1470 return true;
1471 }
1472
1473 @Override
1474 public void onScaleEnd(ScaleGestureDetector detector) {
1475 mWebView.loadUrl(String.format("javascript:onScaleEnd(%s, %s);",
1476 getFocusXWebPx(detector), getFocusYWebPx(detector)));
1477 }
1478
1479 }
1480
Mindy Pereira9b875682012-02-15 18:10:54 -08001481}