blob: ce754789a89ae003b62c19b7f4362377552a3e0d [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
Paul Westbrookb8361c92012-09-27 10:57:14 -070020import android.content.ContentResolver;
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;
Andy Huang9d3fd922012-09-26 22:23:58 -070025import android.database.DataSetObserver;
Paul Westbrookb8361c92012-09-27 10:57:14 -070026import android.net.Uri;
Paul Westbrookcebcc642012-08-08 10:06:04 -070027import android.os.AsyncTask;
Mindy Pereira9b875682012-02-15 18:10:54 -080028import android.os.Bundle;
mindyp3bcf1802012-09-09 11:17:00 -070029import android.os.SystemClock;
Andy Huang47aa9c92012-07-31 15:37:21 -070030import android.text.TextUtils;
Mindy Pereira9b875682012-02-15 18:10:54 -080031import android.view.LayoutInflater;
Andy Huang02f9d182012-11-28 22:38:02 -080032import android.view.ScaleGestureDetector;
Andy Huang02f9d182012-11-28 22:38:02 -080033import android.view.ScaleGestureDetector.OnScaleGestureListener;
Andy Huangc1fb9a92013-02-11 13:09:12 -080034import android.view.View;
Mark Wei4071c2f2012-09-26 14:38:38 -070035import android.view.View.OnLayoutChangeListener;
Mindy Pereira9b875682012-02-15 18:10:54 -080036import android.view.ViewGroup;
Andy Huangf70fc402012-02-17 15:37:42 -080037import android.webkit.ConsoleMessage;
Paul Westbrookcebcc642012-08-08 10:06:04 -070038import android.webkit.CookieManager;
39import android.webkit.CookieSyncManager;
Mindy Pereira974c9662012-09-14 10:02:08 -070040import android.webkit.JavascriptInterface;
Andy Huangf70fc402012-02-17 15:37:42 -080041import android.webkit.WebChromeClient;
42import android.webkit.WebSettings;
Andy Huang17a9cde2012-03-09 18:03:16 -080043import android.webkit.WebView;
Andrew Sapperstein821fa872013-08-21 21:57:39 -070044import android.widget.Button;
Mindy Pereira9b875682012-02-15 18:10:54 -080045
Andy Huang59e0b182012-08-14 14:32:23 -070046import com.android.mail.FormattedDateBuilder;
Mindy Pereira9b875682012-02-15 18:10:54 -080047import com.android.mail.R;
Andy Huang5ff63742012-03-16 20:30:23 -070048import com.android.mail.browse.ConversationContainer;
Andy Huangadbf3e82012-10-13 13:30:19 -070049import com.android.mail.browse.ConversationContainer.OverlayPosition;
Andrew Sapperstein8812d3c2013-06-04 17:06:41 -070050import com.android.mail.browse.ConversationMessage;
Andy Huang46dfba62012-04-19 01:47:32 -070051import com.android.mail.browse.ConversationOverlayItem;
Andy Huang7bdc3752012-03-25 17:18:19 -070052import com.android.mail.browse.ConversationViewAdapter;
Andrew Sapperstein14f93742013-07-25 14:29:56 -070053import com.android.mail.browse.ConversationViewAdapter.BorderItem;
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 Huang59e0b182012-08-14 14:32:23 -070061import com.android.mail.browse.MessageHeaderView;
Andy Huangadbf3e82012-10-13 13:30:19 -070062import com.android.mail.browse.ScrollIndicatorsView;
Andy Huang46dfba62012-04-19 01:47:32 -070063import com.android.mail.browse.SuperCollapsedBlock;
Andy Huang0b7ed6f2012-07-25 19:23:26 -070064import com.android.mail.browse.WebViewContextMenu;
Paul Westbrookc42ad5e2013-05-09 16:52:15 -070065import com.android.mail.content.ObjectCursor;
Mindy Pereira9b875682012-02-15 18:10:54 -080066import com.android.mail.providers.Account;
Andy Huang65fe28f2012-04-06 18:08:53 -070067import com.android.mail.providers.Address;
Mindy Pereira9b875682012-02-15 18:10:54 -080068import com.android.mail.providers.Conversation;
Andy Huangf70fc402012-02-17 15:37:42 -080069import com.android.mail.providers.Message;
Paul Westbrookb8361c92012-09-27 10:57:14 -070070import com.android.mail.providers.UIProvider;
Andy Huangcd5c5ee2012-08-12 19:03:51 -070071import com.android.mail.ui.ConversationViewState.ExpansionState;
Andrew Sapperstein376294b2013-06-06 16:04:26 -070072import com.android.mail.utils.ConversationViewUtils;
Paul Westbrookb334c902012-06-25 11:42:46 -070073import com.android.mail.utils.LogTag;
Mindy Pereira9b875682012-02-15 18:10:54 -080074import com.android.mail.utils.LogUtils;
Andy Huang2e9acfe2012-03-15 22:39:36 -070075import com.android.mail.utils.Utils;
Andy Huang543e7092013-04-22 11:44:56 -070076import com.google.common.collect.ImmutableList;
Andy Huang46dfba62012-04-19 01:47:32 -070077import com.google.common.collect.Lists;
Andy Huang05c70c82013-03-14 15:15:50 -070078import com.google.common.collect.Maps;
Andy Huangb8331b42012-07-16 19:08:53 -070079import com.google.common.collect.Sets;
Andy Huang65fe28f2012-04-06 18:08:53 -070080
Scott Kennedyeb9a4bd2012-11-12 10:33:04 -080081import java.util.ArrayList;
Andy Huang46dfba62012-04-19 01:47:32 -070082import java.util.List;
Andy Huang05c70c82013-03-14 15:15:50 -070083import java.util.Map;
Andy Huangb8331b42012-07-16 19:08:53 -070084import java.util.Set;
Mindy Pereira9b875682012-02-15 18:10:54 -080085
86/**
87 * The conversation view UI component.
88 */
Andrew Sapperstein9f957f32013-07-19 15:18:18 -070089public class ConversationViewFragment extends AbstractConversationViewFragment implements
Andrew Sapperstein4ddda2f2013-06-10 11:15:38 -070090 SuperCollapsedBlock.OnClickListener, OnLayoutChangeListener,
91 MessageHeaderView.MessageHeaderViewCallbacks {
Mindy Pereira8e915722012-02-16 14:42:56 -080092
Paul Westbrookb334c902012-06-25 11:42:46 -070093 private static final String LOG_TAG = LogTag.getLogTag();
Andy Huang632721e2012-04-11 16:57:26 -070094 public static final String LAYOUT_TAG = "ConvLayout";
Mindy Pereira9b875682012-02-15 18:10:54 -080095
Andy Huang3c276bf2013-02-19 14:24:43 -080096 private static final boolean ENABLE_CSS_ZOOM = false;
97
Andy Huang9d3fd922012-09-26 22:23:58 -070098 /**
mindyp1b3cc472012-09-27 11:32:59 -070099 * Difference in the height of the message header whose details have been expanded/collapsed
100 */
101 private int mDiff = 0;
102
103 /**
Andy Huang9d3fd922012-09-26 22:23:58 -0700104 * Default value for {@link #mLoadWaitReason}. Conversation load will happen immediately.
105 */
106 private final int LOAD_NOW = 0;
107 /**
108 * Value for {@link #mLoadWaitReason} that means we are offscreen and waiting for the visible
109 * conversation to finish loading before beginning our load.
110 * <p>
111 * When this value is set, the fragment should register with {@link ConversationListCallbacks}
112 * to know when the visible conversation is loaded. When it is unset, it should unregister.
113 */
114 private final int LOAD_WAIT_FOR_INITIAL_CONVERSATION = 1;
115 /**
116 * Value for {@link #mLoadWaitReason} used when a conversation is too heavyweight to load at
117 * all when not visible (e.g. requires network fetch, or too complex). Conversation load will
118 * wait until this fragment is visible.
119 */
120 private final int LOAD_WAIT_UNTIL_VISIBLE = 2;
mindyp3bcf1802012-09-09 11:17:00 -0700121
Andrew Sapperstein9f957f32013-07-19 15:18:18 -0700122 protected ConversationContainer mConversationContainer;
Mindy Pereira9b875682012-02-15 18:10:54 -0800123
Andrew Sapperstein9f957f32013-07-19 15:18:18 -0700124 protected ConversationWebView mWebView;
Mindy Pereira9b875682012-02-15 18:10:54 -0800125
Mark Wei56d83852012-09-19 14:28:50 -0700126 private ScrollIndicatorsView mScrollIndicators;
127
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700128 private ConversationViewProgressController mProgressController;
129
Andrew Sapperstein821fa872013-08-21 21:57:39 -0700130 private Button mNewMessageBar;
Andy Huang47aa9c92012-07-31 15:37:21 -0700131
Andrew Sapperstein9f957f32013-07-19 15:18:18 -0700132 protected HtmlConversationTemplates mTemplates;
Andy Huangf70fc402012-02-17 15:37:42 -0800133
Andy Huangf70fc402012-02-17 15:37:42 -0800134 private final MailJsBridge mJsBridge = new MailJsBridge();
135
Andrew Sapperstein9f957f32013-07-19 15:18:18 -0700136 protected ConversationViewAdapter mAdapter;
Andy Huang51067132012-03-12 20:08:19 -0700137
Andrew Sappersteinb1d184d2013-08-09 14:14:31 -0700138 protected boolean mViewsCreated;
Mark Wei4071c2f2012-09-26 14:38:38 -0700139 // True if we attempted to render before the views were laid out
140 // We will render immediately once layout is done
141 private boolean mNeedRender;
Andy Huang51067132012-03-12 20:08:19 -0700142
Andy Huang46dfba62012-04-19 01:47:32 -0700143 /**
144 * Temporary string containing the message bodies of the messages within a super-collapsed
145 * block, for one-time use during block expansion. We cannot easily pass the body HTML
146 * into JS without problematic escaping, so hold onto it momentarily and signal JS to fetch it
147 * using {@link MailJsBridge}.
148 */
149 private String mTempBodiesHtml;
150
Andy Huang632721e2012-04-11 16:57:26 -0700151 private int mMaxAutoLoadMessages;
152
Andrew Sapperstein9f957f32013-07-19 15:18:18 -0700153 protected int mSideMarginPx;
Andy Huang02f9d182012-11-28 22:38:02 -0800154
Andy Huang9d3fd922012-09-26 22:23:58 -0700155 /**
156 * If this conversation fragment is not visible, and it's inappropriate to load up front,
157 * this is the reason we are waiting. This flag should be cleared once it's okay to load
158 * the conversation.
159 */
160 private int mLoadWaitReason = LOAD_NOW;
Andy Huang632721e2012-04-11 16:57:26 -0700161
mindyp3bcf1802012-09-09 11:17:00 -0700162 private boolean mEnableContentReadySignal;
Andy Huang28b7aee2012-08-20 20:27:32 -0700163
mindypdde3f9f2012-09-10 17:35:35 -0700164 private ContentSizeChangeListener mWebViewSizeChangeListener;
165
Andy Huange964eee2012-10-02 19:24:58 -0700166 private float mWebViewYPercent;
167
168 /**
169 * Has loadData been called on the WebView yet?
170 */
171 private boolean mWebViewLoadedData;
172
Andy Huang63b3c672012-10-05 19:27:28 -0700173 private long mWebViewLoadStartMs;
174
Andy Huang05c70c82013-03-14 15:15:50 -0700175 private final Map<String, String> mMessageTransforms = Maps.newHashMap();
176
Andy Huang9d3fd922012-09-26 22:23:58 -0700177 private final DataSetObserver mLoadedObserver = new DataSetObserver() {
178 @Override
179 public void onChanged() {
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700180 getHandler().post(new FragmentRunnable("delayedConversationLoad",
181 ConversationViewFragment.this) {
Andy Huang9d3fd922012-09-26 22:23:58 -0700182 @Override
183 public void go() {
184 LogUtils.d(LOG_TAG, "CVF load observer fired, this=%s",
185 ConversationViewFragment.this);
186 handleDelayedConversationLoad();
187 }
188 });
189 }
190 };
Andy Huangf70fc402012-02-17 15:37:42 -0800191
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700192 private final Runnable mOnProgressDismiss = new FragmentRunnable("onProgressDismiss", this) {
Andy Huang7d4746e2012-10-17 17:03:17 -0700193 @Override
194 public void go() {
Scott Kennedy58192e52013-05-08 16:35:57 -0700195 LogUtils.d(LOG_TAG, "onProgressDismiss go() - isUserVisible() = %b", isUserVisible());
Andy Huang7d4746e2012-10-17 17:03:17 -0700196 if (isUserVisible()) {
197 onConversationSeen();
198 }
Andy Huang30bcfe72012-10-18 18:09:03 -0700199 mWebView.onRenderComplete();
Andy Huang7d4746e2012-10-17 17:03:17 -0700200 }
201 };
202
Andy Huangbd544e32012-05-29 15:56:51 -0700203 private static final boolean DEBUG_DUMP_CONVERSATION_HTML = false;
Andy Huang47aa9c92012-07-31 15:37:21 -0700204 private static final boolean DISABLE_OFFSCREEN_LOADING = false;
Andy Huang06c03622012-10-22 18:59:45 -0700205 private static final boolean DEBUG_DUMP_CURSOR_CONTENTS = false;
Andy Huange964eee2012-10-02 19:24:58 -0700206
207 private static final String BUNDLE_KEY_WEBVIEW_Y_PERCENT =
208 ConversationViewFragment.class.getName() + "webview-y-percent";
Andy Huangbd544e32012-05-29 15:56:51 -0700209
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800210 /**
211 * Constructor needs to be public to handle orientation changes and activity lifecycle events.
212 */
Paul Westbrookf0ea4842013-08-13 16:41:18 -0700213 public ConversationViewFragment() {}
Mindy Pereira9b875682012-02-15 18:10:54 -0800214
215 /**
216 * Creates a new instance of {@link ConversationViewFragment}, initialized
Andy Huang632721e2012-04-11 16:57:26 -0700217 * to display a conversation with other parameters inherited/copied from an existing bundle,
218 * typically one created using {@link #makeBasicArgs}.
219 */
220 public static ConversationViewFragment newInstance(Bundle existingArgs,
221 Conversation conversation) {
222 ConversationViewFragment f = new ConversationViewFragment();
223 Bundle args = new Bundle(existingArgs);
224 args.putParcelable(ARG_CONVERSATION, conversation);
225 f.setArguments(args);
226 return f;
227 }
228
mindypf4fce122012-09-14 15:55:33 -0700229 @Override
Andy Huangadbf3e82012-10-13 13:30:19 -0700230 public void onAccountChanged(Account newAccount, Account oldAccount) {
231 // if overview mode has changed, re-render completely (no need to also update headers)
232 if (isOverviewMode(newAccount) != isOverviewMode(oldAccount)) {
233 setupOverviewMode();
234 final MessageCursor c = getMessageCursor();
235 if (c != null) {
236 renderConversation(c);
237 } else {
238 // Null cursor means this fragment is either waiting to load or in the middle of
239 // loading. Either way, a future render will happen anyway, and the new setting
240 // will take effect when that happens.
241 }
242 return;
243 }
244
mindypf4fce122012-09-14 15:55:33 -0700245 // settings may have been updated; refresh views that are known to
246 // depend on settings
mindypf4fce122012-09-14 15:55:33 -0700247 mAdapter.notifyDataSetChanged();
Andy Huang632721e2012-04-11 16:57:26 -0700248 }
249
Mindy Pereira9b875682012-02-15 18:10:54 -0800250 @Override
251 public void onActivityCreated(Bundle savedInstanceState) {
Andy Huang9d3fd922012-09-26 22:23:58 -0700252 LogUtils.d(LOG_TAG, "IN CVF.onActivityCreated, this=%s visible=%s", this, isUserVisible());
Mindy Pereira9b875682012-02-15 18:10:54 -0800253 super.onActivityCreated(savedInstanceState);
Mark Wei1abfcaf2012-09-27 11:11:07 -0700254
255 if (mActivity == null || mActivity.isFinishing()) {
256 // Activity is finishing, just bail.
257 return;
258 }
259
mindypf4fce122012-09-14 15:55:33 -0700260 Context context = getContext();
261 mTemplates = new HtmlConversationTemplates(context);
Andy Huang59e0b182012-08-14 14:32:23 -0700262
mindypf4fce122012-09-14 15:55:33 -0700263 final FormattedDateBuilder dateBuilder = new FormattedDateBuilder(context);
Andy Huang59e0b182012-08-14 14:32:23 -0700264
Paul Westbrook8081df42012-09-10 15:43:36 -0700265 mAdapter = new ConversationViewAdapter(mActivity, this,
mindypf4fce122012-09-14 15:55:33 -0700266 getLoaderManager(), this, getContactInfoSource(), this,
Paul Westbrook8081df42012-09-10 15:43:36 -0700267 this, mAddressCache, dateBuilder);
Andy Huang51067132012-03-12 20:08:19 -0700268 mConversationContainer.setOverlayAdapter(mAdapter);
269
Andy Huang59e0b182012-08-14 14:32:23 -0700270 // set up snap header (the adapter usually does this with the other ones)
271 final MessageHeaderView snapHeader = mConversationContainer.getSnapHeader();
Andrew Sapperstein14f93742013-07-25 14:29:56 -0700272 initHeaderView(snapHeader);
Andy Huangc1fb9a92013-02-11 13:09:12 -0800273
Andrew Sapperstein90eccf92013-08-22 11:53:23 -0700274 final Resources resources = getResources();
275 mMaxAutoLoadMessages = resources.getInteger(R.integer.max_auto_load_messages);
Andy Huang632721e2012-04-11 16:57:26 -0700276
Andrew Sapperstein90eccf92013-08-22 11:53:23 -0700277 mSideMarginPx = resources.getDimensionPixelOffset(
Andy Huang02f9d182012-11-28 22:38:02 -0800278 R.dimen.conversation_message_content_margin_side);
279
mindypf4fce122012-09-14 15:55:33 -0700280 mWebView.setOnCreateContextMenuListener(new WebViewContextMenu(getActivity()));
Andy Huang0b7ed6f2012-07-25 19:23:26 -0700281
Andy Huangadbf3e82012-10-13 13:30:19 -0700282 // set this up here instead of onCreateView to ensure the latest Account is loaded
283 setupOverviewMode();
284
Andy Huang9d3fd922012-09-26 22:23:58 -0700285 // Defer the call to initLoader with a Handler.
286 // We want to wait until we know which fragments are present and their final visibility
287 // states before going off and doing work. This prevents extraneous loading from occurring
288 // as the ViewPager shifts about before the initial position is set.
289 //
290 // e.g. click on item #10
291 // ViewPager.setAdapter() actually first loads #0 and #1 under the assumption that #0 is
292 // the initial primary item
293 // Then CPC immediately sets the primary item to #10, which tears down #0/#1 and sets up
294 // #9/#10/#11.
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700295 getHandler().post(new FragmentRunnable("showConversation", this) {
Andy Huang9d3fd922012-09-26 22:23:58 -0700296 @Override
297 public void go() {
298 showConversation();
299 }
300 });
Paul Westbrookcebcc642012-08-08 10:06:04 -0700301
Andrew Sapperstein606dbd72013-07-30 19:14:23 -0700302 if (mConversation != null && mConversation.conversationBaseUri != null &&
Paul Westbrookb8361c92012-09-27 10:57:14 -0700303 !Utils.isEmpty(mAccount.accoutCookieQueryUri)) {
Paul Westbrookcebcc642012-08-08 10:06:04 -0700304 // Set the cookie for this base url
Paul Westbrookb8361c92012-09-27 10:57:14 -0700305 new SetCookieTask(getContext(), mConversation.conversationBaseUri,
306 mAccount.accoutCookieQueryUri).execute();
Paul Westbrookcebcc642012-08-08 10:06:04 -0700307 }
Mindy Pereira9b875682012-02-15 18:10:54 -0800308 }
309
Andrew Sapperstein14f93742013-07-25 14:29:56 -0700310 private void initHeaderView(MessageHeaderView headerView) {
311 headerView.initialize(this, mAddressCache);
Andy Huangc1fb9a92013-02-11 13:09:12 -0800312 headerView.setCallbacks(this);
313 headerView.setContactInfoSource(getContactInfoSource());
314 headerView.setVeiledMatcher(mActivity.getAccountController().getVeiledAddressMatcher());
315 }
316
Mindy Pereira9b875682012-02-15 18:10:54 -0800317 @Override
Andy Huange964eee2012-10-02 19:24:58 -0700318 public void onCreate(Bundle savedState) {
319 super.onCreate(savedState);
320
Andrew Sappersteinb1d184d2013-08-09 14:14:31 -0700321 mWebViewClient = createConversationWebViewClient();
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700322
Andy Huange964eee2012-10-02 19:24:58 -0700323 if (savedState != null) {
324 mWebViewYPercent = savedState.getFloat(BUNDLE_KEY_WEBVIEW_Y_PERCENT);
325 }
326 }
327
Andrew Sappersteinb1d184d2013-08-09 14:14:31 -0700328 protected ConversationWebViewClient createConversationWebViewClient() {
329 return new ConversationWebViewClient(mAccount);
330 }
331
Andy Huange964eee2012-10-02 19:24:58 -0700332 @Override
Mindy Pereira9b875682012-02-15 18:10:54 -0800333 public View onCreateView(LayoutInflater inflater,
334 ViewGroup container, Bundle savedInstanceState) {
Andy Huang839ada22012-07-20 15:48:40 -0700335
Andy Huang632721e2012-04-11 16:57:26 -0700336 View rootView = inflater.inflate(R.layout.conversation_view, container, false);
Andy Huangf70fc402012-02-17 15:37:42 -0800337 mConversationContainer = (ConversationContainer) rootView
338 .findViewById(R.id.conversation_container);
Andy Huang8f187782012-11-06 17:49:25 -0800339 mConversationContainer.setAccountController(this);
Andy Huang47aa9c92012-07-31 15:37:21 -0700340
Andrew Sapperstein821fa872013-08-21 21:57:39 -0700341 mNewMessageBar = (Button) mConversationContainer.findViewById(R.id.new_message_notification_bar);
Andy Huang47aa9c92012-07-31 15:37:21 -0700342 mNewMessageBar.setOnClickListener(new View.OnClickListener() {
343 @Override
344 public void onClick(View v) {
345 onNewMessageBarClick();
346 }
347 });
348
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700349 mProgressController = new ConversationViewProgressController(this, getHandler());
350 mProgressController.instantiateProgressIndicators(rootView);
mindyp3bcf1802012-09-09 11:17:00 -0700351
Andy Huang5ff63742012-03-16 20:30:23 -0700352 mWebView = (ConversationWebView) mConversationContainer.findViewById(R.id.webview);
Andy Huangf70fc402012-02-17 15:37:42 -0800353
Andy Huangf70fc402012-02-17 15:37:42 -0800354 mWebView.addJavascriptInterface(mJsBridge, "mail");
mindyp3bcf1802012-09-09 11:17:00 -0700355 // On JB or newer, we use the 'webkitAnimationStart' DOM event to signal load complete
356 // Below JB, try to speed up initial render by having the webview do supplemental draws to
357 // custom a software canvas.
mindypb941fdb2012-09-11 08:28:23 -0700358 // TODO(mindyp):
359 //PAGE READINESS SIGNAL FOR JELLYBEAN AND NEWER
360 // Notify the app on 'webkitAnimationStart' of a simple dummy element with a simple no-op
361 // animation that immediately runs on page load. The app uses this as a signal that the
362 // content is loaded and ready to draw, since WebView delays firing this event until the
363 // layers are composited and everything is ready to draw.
364 // This signal does not seem to be reliable, so just use the old method for now.
Andy Huangf7ac83f2013-07-15 15:48:31 -0700365 final boolean isJBOrLater = Utils.isRunningJellybeanOrLater();
366 final boolean isUserVisible = isUserVisible();
367 mWebView.setUseSoftwareLayer(!isJBOrLater);
368 mEnableContentReadySignal = isJBOrLater && isUserVisible;
369 mWebView.onUserVisibilityChanged(isUserVisible);
Andy Huang17a9cde2012-03-09 18:03:16 -0800370 mWebView.setWebViewClient(mWebViewClient);
Andy Huangc1fb9a92013-02-11 13:09:12 -0800371 final WebChromeClient wcc = new WebChromeClient() {
Andy Huangf70fc402012-02-17 15:37:42 -0800372 @Override
373 public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
Andy Huang5ea5a832013-03-07 16:49:09 -0800374 LogUtils.i(LOG_TAG, "JS: %s (%s:%d) f=%s", consoleMessage.message(),
375 consoleMessage.sourceId(), consoleMessage.lineNumber(),
376 ConversationViewFragment.this);
Andy Huangf70fc402012-02-17 15:37:42 -0800377 return true;
378 }
Andy Huangc1fb9a92013-02-11 13:09:12 -0800379 };
380 mWebView.setWebChromeClient(wcc);
Andy Huangf70fc402012-02-17 15:37:42 -0800381
Andy Huang3233bff2012-03-20 19:38:45 -0700382 final WebSettings settings = mWebView.getSettings();
Andy Huangf70fc402012-02-17 15:37:42 -0800383
Mark Wei56d83852012-09-19 14:28:50 -0700384 mScrollIndicators = (ScrollIndicatorsView) rootView.findViewById(R.id.scroll_indicators);
385 mScrollIndicators.setSourceView(mWebView);
386
Andy Huangf70fc402012-02-17 15:37:42 -0800387 settings.setJavaScriptEnabled(true);
Andy Huangf70fc402012-02-17 15:37:42 -0800388
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700389 ConversationViewUtils.setTextZoom(getResources(), settings);
Andy Huangc319b552012-04-25 19:53:50 -0700390
Andy Huang51067132012-03-12 20:08:19 -0700391 mViewsCreated = true;
Andy Huange964eee2012-10-02 19:24:58 -0700392 mWebViewLoadedData = false;
Andy Huang51067132012-03-12 20:08:19 -0700393
Mindy Pereira9b875682012-02-15 18:10:54 -0800394 return rootView;
395 }
396
397 @Override
Andy Huangf7ac83f2013-07-15 15:48:31 -0700398 public void onResume() {
399 super.onResume();
400 if (mWebView != null) {
401 mWebView.onResume();
402 }
403 }
404
405 @Override
406 public void onPause() {
407 super.onPause();
408 if (mWebView != null) {
409 mWebView.onPause();
410 }
411 }
412
413 @Override
Mindy Pereira9b875682012-02-15 18:10:54 -0800414 public void onDestroyView() {
Mindy Pereira9b875682012-02-15 18:10:54 -0800415 super.onDestroyView();
Andy Huang46dfba62012-04-19 01:47:32 -0700416 mConversationContainer.setOverlayAdapter(null);
417 mAdapter = null;
Andy Huang9d3fd922012-09-26 22:23:58 -0700418 resetLoadWaiting(); // be sure to unregister any active load observer
Andy Huang51067132012-03-12 20:08:19 -0700419 mViewsCreated = false;
Mindy Pereira9b875682012-02-15 18:10:54 -0800420 }
421
Andy Huange964eee2012-10-02 19:24:58 -0700422 @Override
423 public void onSaveInstanceState(Bundle outState) {
424 super.onSaveInstanceState(outState);
425
426 outState.putFloat(BUNDLE_KEY_WEBVIEW_Y_PERCENT, calculateScrollYPercent());
427 }
428
429 private float calculateScrollYPercent() {
Paul Westbrook1b56a672013-04-19 01:19:05 -0700430 final float p;
431 if (mWebView == null) {
432 // onCreateView hasn't been called, return 0 as the user hasn't scrolled the view.
433 return 0;
434 }
435
436 final int scrollY = mWebView.getScrollY();
437 final int viewH = mWebView.getHeight();
438 final int webH = (int) (mWebView.getContentHeight() * mWebView.getScale());
Andy Huange964eee2012-10-02 19:24:58 -0700439
440 if (webH == 0 || webH <= viewH) {
441 p = 0;
442 } else if (scrollY + viewH >= webH) {
443 // The very bottom is a special case, it acts as a stronger anchor than the scroll top
444 // at that point.
445 p = 1.0f;
446 } else {
447 p = (float) scrollY / webH;
448 }
449 return p;
450 }
451
Andy Huang9d3fd922012-09-26 22:23:58 -0700452 private void resetLoadWaiting() {
453 if (mLoadWaitReason == LOAD_WAIT_FOR_INITIAL_CONVERSATION) {
454 getListController().unregisterConversationLoadedObserver(mLoadedObserver);
455 }
456 mLoadWaitReason = LOAD_NOW;
457 }
458
Andy Huang5ff63742012-03-16 20:30:23 -0700459 @Override
mindypf4fce122012-09-14 15:55:33 -0700460 protected void markUnread() {
Vikram Aggarwald82a31f2013-02-05 15:03:00 -0800461 super.markUnread();
Andy Huang839ada22012-07-20 15:48:40 -0700462 // Ignore unsafe calls made after a fragment is detached from an activity
463 final ControllableActivity activity = (ControllableActivity) getActivity();
464 if (activity == null) {
465 LogUtils.w(LOG_TAG, "ignoring markUnread for conv=%s", mConversation.id);
466 return;
467 }
468
Andy Huang28e31e22012-07-26 16:33:15 -0700469 if (mViewState == null) {
470 LogUtils.i(LOG_TAG, "ignoring markUnread for conv with no view state (%d)",
471 mConversation.id);
472 return;
473 }
Andy Huang839ada22012-07-20 15:48:40 -0700474 activity.getConversationUpdater().markConversationMessagesUnread(mConversation,
Vikram Aggarwal4a878b62012-07-31 15:09:25 -0700475 mViewState.getUnreadMessageUris(), mViewState.getConversationInfo());
Andy Huang839ada22012-07-20 15:48:40 -0700476 }
477
mindypf4fce122012-09-14 15:55:33 -0700478 @Override
479 public void onUserVisibleHintChanged() {
Andy Huang9d3fd922012-09-26 22:23:58 -0700480 final boolean userVisible = isUserVisible();
Scott Kennedy58192e52013-05-08 16:35:57 -0700481 LogUtils.d(LOG_TAG, "ConversationViewFragment#onUserVisibleHintChanged(), userVisible = %b",
482 userVisible);
Andy Huang9d3fd922012-09-26 22:23:58 -0700483
484 if (!userVisible) {
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700485 mProgressController.dismissLoadingStatus();
Andy Huang9d3fd922012-09-26 22:23:58 -0700486 } else if (mViewsCreated) {
487 if (getMessageCursor() != null) {
488 LogUtils.d(LOG_TAG, "Fragment is now user-visible, onConversationSeen: %s", this);
489 onConversationSeen();
490 } else if (isLoadWaiting()) {
491 LogUtils.d(LOG_TAG, "Fragment is now user-visible, showing conversation: %s", this);
492 handleDelayedConversationLoad();
493 }
Andy Huang632721e2012-04-11 16:57:26 -0700494 }
Andy Huang632721e2012-04-11 16:57:26 -0700495
Andy Huang30bcfe72012-10-18 18:09:03 -0700496 if (mWebView != null) {
497 mWebView.onUserVisibilityChanged(userVisible);
498 }
Andy Huangf8cf5462012-10-17 18:29:14 -0700499 }
500
Andy Huang9d3fd922012-09-26 22:23:58 -0700501 /**
502 * Will either call initLoader now to begin loading, or set {@link #mLoadWaitReason} and do
503 * nothing (in which case you should later call {@link #handleDelayedConversationLoad()}).
504 */
Mindy Pereira9b875682012-02-15 18:10:54 -0800505 private void showConversation() {
Andy Huang9d3fd922012-09-26 22:23:58 -0700506 final int reason;
507
508 if (isUserVisible()) {
509 LogUtils.i(LOG_TAG,
510 "SHOWCONV: CVF is user-visible, immediately loading conversation (%s)", this);
511 reason = LOAD_NOW;
Andy Huang243c2362013-03-01 17:50:35 -0800512 timerMark("CVF.showConversation");
Andy Huang9d3fd922012-09-26 22:23:58 -0700513 } else {
514 final boolean disableOffscreenLoading = DISABLE_OFFSCREEN_LOADING
Andrew Sapperstein606dbd72013-07-30 19:14:23 -0700515 || (mConversation != null && (mConversation.isRemote
516 || mConversation.getNumMessages() > mMaxAutoLoadMessages));
Andy Huang9d3fd922012-09-26 22:23:58 -0700517
518 // When not visible, we should not immediately load if either this conversation is
519 // too heavyweight, or if the main/initial conversation is busy loading.
520 if (disableOffscreenLoading) {
521 reason = LOAD_WAIT_UNTIL_VISIBLE;
522 LogUtils.i(LOG_TAG, "SHOWCONV: CVF waiting until visible to load (%s)", this);
523 } else if (getListController().isInitialConversationLoading()) {
524 reason = LOAD_WAIT_FOR_INITIAL_CONVERSATION;
525 LogUtils.i(LOG_TAG, "SHOWCONV: CVF waiting for initial to finish (%s)", this);
526 getListController().registerConversationLoadedObserver(mLoadedObserver);
527 } else {
528 LogUtils.i(LOG_TAG,
529 "SHOWCONV: CVF is not visible, but no reason to wait. loading now. (%s)",
530 this);
531 reason = LOAD_NOW;
532 }
Andy Huang632721e2012-04-11 16:57:26 -0700533 }
Andy Huang9d3fd922012-09-26 22:23:58 -0700534
535 mLoadWaitReason = reason;
536 if (mLoadWaitReason == LOAD_NOW) {
537 startConversationLoad();
538 }
539 }
540
541 private void handleDelayedConversationLoad() {
542 resetLoadWaiting();
543 startConversationLoad();
544 }
545
546 private void startConversationLoad() {
mindyp3bcf1802012-09-09 11:17:00 -0700547 mWebView.setVisibility(View.VISIBLE);
Andrew Sapperstein606dbd72013-07-30 19:14:23 -0700548 loadContent();
mindyp3bcf1802012-09-09 11:17:00 -0700549 // TODO(mindyp): don't show loading status for a previously rendered
550 // conversation. Ielieve this is better done by making sure don't show loading status
551 // until XX ms have passed without loading completed.
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700552 mProgressController.showLoadingStatus(isUserVisible());
Mindy Pereira8e915722012-02-16 14:42:56 -0800553 }
554
Andrew Sapperstein606dbd72013-07-30 19:14:23 -0700555 /**
556 * Can be overridden in case a subclass needs to load something other than
557 * the messages of a conversation.
558 */
559 protected void loadContent() {
560 getLoaderManager().initLoader(MESSAGE_LOADER, Bundle.EMPTY, getMessageLoaderCallbacks());
561 }
562
Andy Huang7d4746e2012-10-17 17:03:17 -0700563 private void revealConversation() {
Andy Huang243c2362013-03-01 17:50:35 -0800564 timerMark("revealing conversation");
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700565 mProgressController.dismissLoadingStatus(mOnProgressDismiss);
Andy Huang7d4746e2012-10-17 17:03:17 -0700566 }
567
Andy Huang9d3fd922012-09-26 22:23:58 -0700568 private boolean isLoadWaiting() {
569 return mLoadWaitReason != LOAD_NOW;
570 }
571
Andy Huang51067132012-03-12 20:08:19 -0700572 private void renderConversation(MessageCursor messageCursor) {
mindyp3bcf1802012-09-09 11:17:00 -0700573 final String convHtml = renderMessageBodies(messageCursor, mEnableContentReadySignal);
Andy Huang243c2362013-03-01 17:50:35 -0800574 timerMark("rendered conversation");
Andy Huangbd544e32012-05-29 15:56:51 -0700575
576 if (DEBUG_DUMP_CONVERSATION_HTML) {
577 java.io.FileWriter fw = null;
578 try {
579 fw = new java.io.FileWriter("/sdcard/conv" + mConversation.id
580 + ".html");
581 fw.write(convHtml);
582 } catch (java.io.IOException e) {
583 e.printStackTrace();
584 } finally {
585 if (fw != null) {
586 try {
587 fw.close();
588 } catch (java.io.IOException e) {
589 e.printStackTrace();
590 }
591 }
592 }
593 }
594
Andy Huange964eee2012-10-02 19:24:58 -0700595 // save off existing scroll position before re-rendering
596 if (mWebViewLoadedData) {
597 mWebViewYPercent = calculateScrollYPercent();
598 }
599
Andy Huangbd544e32012-05-29 15:56:51 -0700600 mWebView.loadDataWithBaseURL(mBaseUri, convHtml, "text/html", "utf-8", null);
Andy Huange964eee2012-10-02 19:24:58 -0700601 mWebViewLoadedData = true;
Andy Huang63b3c672012-10-05 19:27:28 -0700602 mWebViewLoadStartMs = SystemClock.uptimeMillis();
Andy Huang51067132012-03-12 20:08:19 -0700603 }
604
Andy Huang7bdc3752012-03-25 17:18:19 -0700605 /**
606 * Populate the adapter with overlay views (message headers, super-collapsed blocks, a
607 * conversation header), and return an HTML document with spacer divs inserted for all overlays.
608 *
609 */
Andrew Sapperstein9f957f32013-07-19 15:18:18 -0700610 protected String renderMessageBodies(MessageCursor messageCursor,
mindyp3bcf1802012-09-09 11:17:00 -0700611 boolean enableContentReadySignal) {
Andy Huangf70fc402012-02-17 15:37:42 -0800612 int pos = -1;
Andy Huang632721e2012-04-11 16:57:26 -0700613
Andy Huang1ee96b22012-08-24 20:19:53 -0700614 LogUtils.d(LOG_TAG, "IN renderMessageBodies, fragment=%s", this);
Andy Huang7bdc3752012-03-25 17:18:19 -0700615 boolean allowNetworkImages = false;
616
Andy Huangc7543572012-04-03 15:34:29 -0700617 // TODO: re-use any existing adapter item state (expanded, details expanded, show pics)
Andy Huang28b7aee2012-08-20 20:27:32 -0700618
Andy Huang7bdc3752012-03-25 17:18:19 -0700619 // Walk through the cursor and build up an overlay adapter as you go.
620 // Each overlay has an entry in the adapter for easy scroll handling in the container.
621 // Items are not necessarily 1:1 in cursor and adapter because of super-collapsed blocks.
622 // When adding adapter items, also add their heights to help the container later determine
623 // overlay dimensions.
624
Andy Huangdb620fe2012-08-24 15:45:28 -0700625 // When re-rendering, prevent ConversationContainer from laying out overlays until after
626 // the new spacers are positioned by WebView.
627 mConversationContainer.invalidateSpacerGeometry();
628
Andy Huang7bdc3752012-03-25 17:18:19 -0700629 mAdapter.clear();
630
Andy Huang47aa9c92012-07-31 15:37:21 -0700631 // re-evaluate the message parts of the view state, since the messages may have changed
632 // since the previous render
633 final ConversationViewState prevState = mViewState;
634 mViewState = new ConversationViewState(prevState);
635
Andy Huang5ff63742012-03-16 20:30:23 -0700636 // N.B. the units of height for spacers are actually dp and not px because WebView assumes
Andy Huang2e9acfe2012-03-15 22:39:36 -0700637 // a pixel is an mdpi pixel, unless you set device-dpi.
Andy Huang5ff63742012-03-16 20:30:23 -0700638
Andy Huang7bdc3752012-03-25 17:18:19 -0700639 // add a single conversation header item
640 final int convHeaderPos = mAdapter.addConversationHeader(mConversation);
Andy Huang23014702012-07-09 12:50:36 -0700641 final int convHeaderPx = measureOverlayHeight(convHeaderPos);
Andy Huang5ff63742012-03-16 20:30:23 -0700642
Andy Huang02f9d182012-11-28 22:38:02 -0800643 mTemplates.startConversation(mWebView.screenPxToWebPx(mSideMarginPx),
Andy Huang256b35c2012-08-22 15:19:13 -0700644 mWebView.screenPxToWebPx(convHeaderPx));
Andy Huang3233bff2012-03-20 19:38:45 -0700645
Andy Huang46dfba62012-04-19 01:47:32 -0700646 int collapsedStart = -1;
Andy Huang839ada22012-07-20 15:48:40 -0700647 ConversationMessage prevCollapsedMsg = null;
Andy Huang46dfba62012-04-19 01:47:32 -0700648 boolean prevSafeForImages = false;
649
Andrew Sappersteincee3c902013-07-31 10:52:02 -0700650 // Store the previous expanded state so that the border between
651 // the previous and current message can be properly initialized.
652 int previousExpandedState = ExpansionState.NONE;
Andy Huangf70fc402012-02-17 15:37:42 -0800653 while (messageCursor.moveToPosition(++pos)) {
Andy Huang839ada22012-07-20 15:48:40 -0700654 final ConversationMessage msg = messageCursor.getMessage();
Andy Huang46dfba62012-04-19 01:47:32 -0700655
Scott Kennedy20273842012-11-07 11:16:21 -0800656 final boolean safeForImages =
657 msg.alwaysShowImages || prevState.getShouldShowImages(msg);
Andy Huang3233bff2012-03-20 19:38:45 -0700658 allowNetworkImages |= safeForImages;
Andy Huang24055282012-03-27 17:37:06 -0700659
Paul Westbrook08098ec2012-08-12 15:30:28 -0700660 final Integer savedExpanded = prevState.getExpansionState(msg);
661 final int expandedState;
Andy Huang839ada22012-07-20 15:48:40 -0700662 if (savedExpanded != null) {
Andy Huang1ee96b22012-08-24 20:19:53 -0700663 if (ExpansionState.isSuperCollapsed(savedExpanded) && messageCursor.isLast()) {
664 // override saved state when this is now the new last message
665 // this happens to the second-to-last message when you discard a draft
666 expandedState = ExpansionState.EXPANDED;
667 } else {
668 expandedState = savedExpanded;
669 }
Andy Huang839ada22012-07-20 15:48:40 -0700670 } else {
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700671 // new messages that are not expanded default to being eligible for super-collapse
Paul Westbrook08098ec2012-08-12 15:30:28 -0700672 expandedState = (!msg.read || msg.starred || messageCursor.isLast()) ?
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700673 ExpansionState.EXPANDED : ExpansionState.SUPER_COLLAPSED;
Andy Huang839ada22012-07-20 15:48:40 -0700674 }
Scott Kennedy20273842012-11-07 11:16:21 -0800675 mViewState.setShouldShowImages(msg, prevState.getShouldShowImages(msg));
Paul Westbrook08098ec2012-08-12 15:30:28 -0700676 mViewState.setExpansionState(msg, expandedState);
Andy Huangc7543572012-04-03 15:34:29 -0700677
Andy Huang839ada22012-07-20 15:48:40 -0700678 // save off "read" state from the cursor
679 // later, the view may not match the cursor (e.g. conversation marked read on open)
Andy Huang423bea22012-08-21 12:00:49 -0700680 // however, if a previous state indicated this message was unread, trust that instead
681 // so "mark unread" marks all originally unread messages
682 mViewState.setReadState(msg, msg.read && !prevState.isUnread(msg));
Andy Huang839ada22012-07-20 15:48:40 -0700683
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700684 // We only want to consider this for inclusion in the super collapsed block if
685 // 1) The we don't have previous state about this message (The first time that the
686 // user opens a conversation)
687 // 2) The previously saved state for this message indicates that this message is
688 // in the super collapsed block.
689 if (ExpansionState.isSuperCollapsed(expandedState)) {
690 // contribute to a super-collapsed block that will be emitted just before the
691 // next expanded header
692 if (collapsedStart < 0) {
693 collapsedStart = pos;
Andy Huang46dfba62012-04-19 01:47:32 -0700694 }
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700695 prevCollapsedMsg = msg;
696 prevSafeForImages = safeForImages;
Andrew Sapperstein7dc7fa02013-05-08 16:39:31 -0700697
698 // This line puts the from address in the address cache so that
699 // we get the sender image for it if it's in a super-collapsed block.
700 getAddress(msg.getFrom());
Andrew Sappersteincee3c902013-07-31 10:52:02 -0700701 previousExpandedState = expandedState;
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700702 continue;
Andy Huang46dfba62012-04-19 01:47:32 -0700703 }
Andy Huang24055282012-03-27 17:37:06 -0700704
Andy Huang46dfba62012-04-19 01:47:32 -0700705 // resolve any deferred decisions on previous collapsed items
706 if (collapsedStart >= 0) {
707 if (pos - collapsedStart == 1) {
Andrew Sappersteincee3c902013-07-31 10:52:02 -0700708 // Special-case for a single collapsed message: no need to super-collapse it.
709 // Since it is super-collapsed, there is no previous message to be
710 // collapsed and the border above it is the first border.
711 renderMessage(prevCollapsedMsg, false /* previousCollapsed */,
712 false /* expanded */, prevSafeForImages, true /* firstBorder */);
Andy Huang46dfba62012-04-19 01:47:32 -0700713 } else {
714 renderSuperCollapsedBlock(collapsedStart, pos - 1);
715 }
716 prevCollapsedMsg = null;
717 collapsedStart = -1;
718 }
Andy Huang7bdc3752012-03-25 17:18:19 -0700719
Andrew Sappersteincee3c902013-07-31 10:52:02 -0700720 renderMessage(msg, ExpansionState.isCollapsed(previousExpandedState),
721 ExpansionState.isExpanded(expandedState), safeForImages,
Andrew Sappersteine5398802013-07-29 20:55:38 -0700722 pos == 0 /* firstBorder */);
Andrew Sappersteincee3c902013-07-31 10:52:02 -0700723
724 previousExpandedState = expandedState;
Mindy Pereira9b875682012-02-15 18:10:54 -0800725 }
Andy Huang3233bff2012-03-20 19:38:45 -0700726
727 mWebView.getSettings().setBlockNetworkImage(!allowNetworkImages);
728
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700729 final boolean applyTransforms = shouldApplyTransforms();
Andy Huang57f354c2013-04-11 17:23:40 -0700730
Andrew Sappersteincee3c902013-07-31 10:52:02 -0700731 renderBorder(true /* contiguous */, true /* expanded */,
732 false /* firstBorder */, true /* lastBorder */);
Andrew Sapperstein14f93742013-07-25 14:29:56 -0700733
Andy Huangc1fb9a92013-02-11 13:09:12 -0800734 // If the conversation has specified a base uri, use it here, otherwise use mBaseUri
Andrew Sapperstein68141df2013-08-19 19:07:14 -0700735 return mTemplates.endConversation(mBaseUri, mConversation.getBaseUri(mBaseUri),
Andy Huang5ea5a832013-03-07 16:49:09 -0800736 mWebView.getViewportWidth(), enableContentReadySignal, isOverviewMode(mAccount),
Andy Huang57f354c2013-04-11 17:23:40 -0700737 applyTransforms, applyTransforms);
Mindy Pereira9b875682012-02-15 18:10:54 -0800738 }
Mindy Pereira674afa42012-02-17 14:05:24 -0800739
Andy Huang46dfba62012-04-19 01:47:32 -0700740 private void renderSuperCollapsedBlock(int start, int end) {
Andrew Sappersteincee3c902013-07-31 10:52:02 -0700741 renderBorder(true /* contiguous */, true /* expanded */,
742 true /* firstBorder */, false /* lastBorder */);
Andy Huang46dfba62012-04-19 01:47:32 -0700743 final int blockPos = mAdapter.addSuperCollapsedBlock(start, end);
Andy Huang23014702012-07-09 12:50:36 -0700744 final int blockPx = measureOverlayHeight(blockPos);
745 mTemplates.appendSuperCollapsedHtml(start, mWebView.screenPxToWebPx(blockPx));
Andy Huang46dfba62012-04-19 01:47:32 -0700746 }
747
Andrew Sappersteincee3c902013-07-31 10:52:02 -0700748 protected void renderBorder(
749 boolean contiguous, boolean expanded, boolean firstBorder, boolean lastBorder) {
750 final int blockPos = mAdapter.addBorder(contiguous, expanded, firstBorder, lastBorder);
Andrew Sapperstein14f93742013-07-25 14:29:56 -0700751 final int blockPx = measureOverlayHeight(blockPos);
752 mTemplates.appendBorder(mWebView.screenPxToWebPx(blockPx));
753 }
754
Andrew Sappersteincee3c902013-07-31 10:52:02 -0700755 private void renderMessage(ConversationMessage msg, boolean previousCollapsed,
756 boolean expanded, boolean safeForImages, boolean firstBorder) {
757 renderMessage(msg, previousCollapsed, expanded, safeForImages,
758 true /* renderBorder */, firstBorder);
Andrew Sapperstein1f082232013-07-29 11:16:07 -0700759 }
760
Andrew Sappersteincee3c902013-07-31 10:52:02 -0700761 private void renderMessage(ConversationMessage msg, boolean previousCollapsed,
762 boolean expanded, boolean safeForImages, boolean renderBorder, boolean firstBorder) {
Andrew Sapperstein1f082232013-07-29 11:16:07 -0700763 if (renderBorder) {
Andrew Sappersteincee3c902013-07-31 10:52:02 -0700764 // The border should be collapsed only if both the current
765 // and previous messages are collapsed.
766 renderBorder(true /* contiguous */, !previousCollapsed || expanded,
767 firstBorder, false /* lastBorder */);
Andrew Sapperstein1f082232013-07-29 11:16:07 -0700768 }
Andrew Sapperstein14f93742013-07-25 14:29:56 -0700769
Scott Kennedy20273842012-11-07 11:16:21 -0800770 final int headerPos = mAdapter.addMessageHeader(msg, expanded,
771 mViewState.getShouldShowImages(msg));
Andy Huang46dfba62012-04-19 01:47:32 -0700772 final MessageHeaderItem headerItem = (MessageHeaderItem) mAdapter.getItem(headerPos);
773
774 final int footerPos = mAdapter.addMessageFooter(headerItem);
775
776 // Measure item header and footer heights to allocate spacers in HTML
777 // But since the views themselves don't exist yet, render each item temporarily into
778 // a host view for measurement.
Andy Huang23014702012-07-09 12:50:36 -0700779 final int headerPx = measureOverlayHeight(headerPos);
780 final int footerPx = measureOverlayHeight(footerPos);
Andy Huang46dfba62012-04-19 01:47:32 -0700781
Andy Huang256b35c2012-08-22 15:19:13 -0700782 mTemplates.appendMessageHtml(msg, expanded, safeForImages,
Andy Huang23014702012-07-09 12:50:36 -0700783 mWebView.screenPxToWebPx(headerPx), mWebView.screenPxToWebPx(footerPx));
Andy Huang243c2362013-03-01 17:50:35 -0800784 timerMark("rendered message");
Andy Huang46dfba62012-04-19 01:47:32 -0700785 }
786
787 private String renderCollapsedHeaders(MessageCursor cursor,
788 SuperCollapsedBlockItem blockToReplace) {
789 final List<ConversationOverlayItem> replacements = Lists.newArrayList();
790
791 mTemplates.reset();
792
Mark Wei2b24e992012-09-10 16:40:07 -0700793 // In devices with non-integral density multiplier, screen pixels translate to non-integral
794 // web pixels. Keep track of the error that occurs when we cast all heights to int
795 float error = 0f;
Andrew Sapperstein14f93742013-07-25 14:29:56 -0700796 boolean first = true;
Andy Huang46dfba62012-04-19 01:47:32 -0700797 for (int i = blockToReplace.getStart(), end = blockToReplace.getEnd(); i <= end; i++) {
798 cursor.moveToPosition(i);
Andy Huang839ada22012-07-20 15:48:40 -0700799 final ConversationMessage msg = cursor.getMessage();
Andrew Sapperstein14f93742013-07-25 14:29:56 -0700800
801 final int borderPx;
802 if (first) {
803 borderPx = 0;
804 first = false;
805 } else {
Andrew Sappersteincee3c902013-07-31 10:52:02 -0700806 // When replacing the super-collapsed block,
807 // the border is always collapsed between messages.
808 final BorderItem border = mAdapter.newBorderItem(
809 true /* contiguous */, false /* expanded */);
Andrew Sapperstein14f93742013-07-25 14:29:56 -0700810 borderPx = measureOverlayHeight(border);
811 replacements.add(border);
812 mTemplates.appendBorder(mWebView.screenPxToWebPx(borderPx));
813 }
814
Andrew Sapperstein4ddda2f2013-06-10 11:15:38 -0700815 final MessageHeaderItem header = ConversationViewAdapter.newMessageHeaderItem(
Andrew Sapperstein14f93742013-07-25 14:29:56 -0700816 mAdapter, mAdapter.getDateBuilder(), msg, false /* expanded */,
817 mViewState.getShouldShowImages(msg));
Andy Huang46dfba62012-04-19 01:47:32 -0700818 final MessageFooterItem footer = mAdapter.newMessageFooterItem(header);
819
Andy Huang23014702012-07-09 12:50:36 -0700820 final int headerPx = measureOverlayHeight(header);
821 final int footerPx = measureOverlayHeight(footer);
Mark Wei2b24e992012-09-10 16:40:07 -0700822 error += mWebView.screenPxToWebPxError(headerPx)
Andrew Sapperstein14f93742013-07-25 14:29:56 -0700823 + mWebView.screenPxToWebPxError(footerPx)
824 + mWebView.screenPxToWebPxError(borderPx);
Mark Wei2b24e992012-09-10 16:40:07 -0700825
826 // When the error becomes greater than 1 pixel, make the next header 1 pixel taller
827 int correction = 0;
828 if (error >= 1) {
829 correction = 1;
830 error -= 1;
831 }
Andy Huang46dfba62012-04-19 01:47:32 -0700832
Andy Huang256b35c2012-08-22 15:19:13 -0700833 mTemplates.appendMessageHtml(msg, false /* expanded */, msg.alwaysShowImages,
Mark Wei2b24e992012-09-10 16:40:07 -0700834 mWebView.screenPxToWebPx(headerPx) + correction,
835 mWebView.screenPxToWebPx(footerPx));
Andy Huang46dfba62012-04-19 01:47:32 -0700836 replacements.add(header);
837 replacements.add(footer);
Andy Huang839ada22012-07-20 15:48:40 -0700838
Paul Westbrook08098ec2012-08-12 15:30:28 -0700839 mViewState.setExpansionState(msg, ExpansionState.COLLAPSED);
Andy Huang46dfba62012-04-19 01:47:32 -0700840 }
841
842 mAdapter.replaceSuperCollapsedBlock(blockToReplace, replacements);
Andy Huang06c03622012-10-22 18:59:45 -0700843 mAdapter.notifyDataSetChanged();
Andy Huang46dfba62012-04-19 01:47:32 -0700844
845 return mTemplates.emit();
846 }
847
Andrew Sapperstein9f957f32013-07-19 15:18:18 -0700848 protected int measureOverlayHeight(int position) {
Andy Huang46dfba62012-04-19 01:47:32 -0700849 return measureOverlayHeight(mAdapter.getItem(position));
850 }
851
Andy Huang7bdc3752012-03-25 17:18:19 -0700852 /**
Andy Huangb8331b42012-07-16 19:08:53 -0700853 * Measure the height of an adapter view by rendering an adapter item into a temporary
Andy Huang46dfba62012-04-19 01:47:32 -0700854 * host view, and asking the view to immediately measure itself. This method will reuse
Andy Huang7bdc3752012-03-25 17:18:19 -0700855 * a previous adapter view from {@link ConversationContainer}'s scrap views if one was generated
856 * earlier.
857 * <p>
Andy Huang46dfba62012-04-19 01:47:32 -0700858 * After measuring the height, this method also saves the height in the
859 * {@link ConversationOverlayItem} for later use in overlay positioning.
Andy Huang7bdc3752012-03-25 17:18:19 -0700860 *
Andy Huang46dfba62012-04-19 01:47:32 -0700861 * @param convItem adapter item with data to render and measure
Andy Huang23014702012-07-09 12:50:36 -0700862 * @return height of the rendered view in screen px
Andy Huang7bdc3752012-03-25 17:18:19 -0700863 */
Andy Huang46dfba62012-04-19 01:47:32 -0700864 private int measureOverlayHeight(ConversationOverlayItem convItem) {
Andy Huang7bdc3752012-03-25 17:18:19 -0700865 final int type = convItem.getType();
866
867 final View convertView = mConversationContainer.getScrapView(type);
Andy Huangb8331b42012-07-16 19:08:53 -0700868 final View hostView = mAdapter.getView(convItem, convertView, mConversationContainer,
869 true /* measureOnly */);
Andy Huang7bdc3752012-03-25 17:18:19 -0700870 if (convertView == null) {
871 mConversationContainer.addScrapView(type, hostView);
872 }
873
Andy Huang9875bb42012-04-04 20:36:21 -0700874 final int heightPx = mConversationContainer.measureOverlay(hostView);
Andy Huang7bdc3752012-03-25 17:18:19 -0700875 convItem.setHeight(heightPx);
Andy Huang9875bb42012-04-04 20:36:21 -0700876 convItem.markMeasurementValid();
Andy Huang7bdc3752012-03-25 17:18:19 -0700877
Andy Huang23014702012-07-09 12:50:36 -0700878 return heightPx;
Andy Huang7bdc3752012-03-25 17:18:19 -0700879 }
880
Andy Huang5ff63742012-03-16 20:30:23 -0700881 @Override
882 public void onConversationViewHeaderHeightChange(int newHeight) {
Mark Weiab2d9982012-09-25 13:06:17 -0700883 final int h = mWebView.screenPxToWebPx(newHeight);
884
885 mWebView.loadUrl(String.format("javascript:setConversationHeaderSpacerHeight(%s);", h));
Andy Huang5ff63742012-03-16 20:30:23 -0700886 }
887
Andy Huang3233bff2012-03-20 19:38:45 -0700888 // END conversation header callbacks
889
890 // START message header callbacks
891 @Override
Andy Huangc7543572012-04-03 15:34:29 -0700892 public void setMessageSpacerHeight(MessageHeaderItem item, int newSpacerHeightPx) {
893 mConversationContainer.invalidateSpacerGeometry();
894
895 // update message HTML spacer height
Andy Huang23014702012-07-09 12:50:36 -0700896 final int h = mWebView.screenPxToWebPx(newSpacerHeightPx);
897 LogUtils.i(LAYOUT_TAG, "setting HTML spacer h=%dwebPx (%dscreenPx)", h,
898 newSpacerHeightPx);
Vikram Aggarwal5349ce12012-09-24 14:12:40 -0700899 mWebView.loadUrl(String.format("javascript:setMessageHeaderSpacerHeight('%s', %s);",
Andy Huang014ea4c2012-09-25 14:50:54 -0700900 mTemplates.getMessageDomId(item.getMessage()), h));
Andy Huang3233bff2012-03-20 19:38:45 -0700901 }
902
903 @Override
Andrew Sappersteincee3c902013-07-31 10:52:02 -0700904 public void setMessageExpanded(MessageHeaderItem item, int newSpacerHeightPx,
905 int topBorderHeight, int bottomBorderHeight) {
Andy Huangc7543572012-04-03 15:34:29 -0700906 mConversationContainer.invalidateSpacerGeometry();
907
908 // show/hide the HTML message body and update the spacer height
Andy Huang23014702012-07-09 12:50:36 -0700909 final int h = mWebView.screenPxToWebPx(newSpacerHeightPx);
Andrew Sappersteincee3c902013-07-31 10:52:02 -0700910 final int topHeight = mWebView.screenPxToWebPx(topBorderHeight);
911 final int bottomHeight = mWebView.screenPxToWebPx(bottomBorderHeight);
Andy Huang23014702012-07-09 12:50:36 -0700912 LogUtils.i(LAYOUT_TAG, "setting HTML spacer expanded=%s h=%dwebPx (%dscreenPx)",
913 item.isExpanded(), h, newSpacerHeightPx);
Andrew Sappersteincee3c902013-07-31 10:52:02 -0700914 mWebView.loadUrl(String.format("javascript:setMessageBodyVisible('%s', %s, %s, %s, %s);",
915 mTemplates.getMessageDomId(item.getMessage()), item.isExpanded(),
916 h, topHeight, bottomHeight));
Andy Huang839ada22012-07-20 15:48:40 -0700917
Andy Huang014ea4c2012-09-25 14:50:54 -0700918 mViewState.setExpansionState(item.getMessage(),
Paul Westbrook08098ec2012-08-12 15:30:28 -0700919 item.isExpanded() ? ExpansionState.EXPANDED : ExpansionState.COLLAPSED);
Andy Huang3233bff2012-03-20 19:38:45 -0700920 }
921
922 @Override
Scott Kennedyeb9a4bd2012-11-12 10:33:04 -0800923 public void showExternalResources(final Message msg) {
Scott Kennedy20273842012-11-07 11:16:21 -0800924 mViewState.setShouldShowImages(msg, true);
Andy Huang3233bff2012-03-20 19:38:45 -0700925 mWebView.getSettings().setBlockNetworkImage(false);
Scott Kennedyeb9a4bd2012-11-12 10:33:04 -0800926 mWebView.loadUrl("javascript:unblockImages(['" + mTemplates.getMessageDomId(msg) + "']);");
927 }
928
929 @Override
930 public void showExternalResources(final String senderRawAddress) {
931 mWebView.getSettings().setBlockNetworkImage(false);
932
933 final Address sender = getAddress(senderRawAddress);
934 final MessageCursor cursor = getMessageCursor();
935
936 final List<String> messageDomIds = new ArrayList<String>();
937
938 int pos = -1;
939 while (cursor.moveToPosition(++pos)) {
940 final ConversationMessage message = cursor.getMessage();
941 if (sender.equals(getAddress(message.getFrom()))) {
942 message.alwaysShowImages = true;
943
944 mViewState.setShouldShowImages(message, true);
945 messageDomIds.add(mTemplates.getMessageDomId(message));
946 }
947 }
948
949 final String url = String.format(
950 "javascript:unblockImages(['%s']);", TextUtils.join("','", messageDomIds));
951 mWebView.loadUrl(url);
Andy Huang3233bff2012-03-20 19:38:45 -0700952 }
Alice Yang1ebc2db2013-03-14 21:21:44 -0700953
954 @Override
Andy Huang75b52a52013-03-15 15:40:24 -0700955 public boolean supportsMessageTransforms() {
956 return true;
957 }
958
959 @Override
Alice Yang1ebc2db2013-03-14 21:21:44 -0700960 public String getMessageTransforms(final Message msg) {
961 final String domId = mTemplates.getMessageDomId(msg);
962 return (domId == null) ? null : mMessageTransforms.get(domId);
963 }
964
Andy Huang3233bff2012-03-20 19:38:45 -0700965 // END message header callbacks
Andy Huang5ff63742012-03-16 20:30:23 -0700966
Andy Huang46dfba62012-04-19 01:47:32 -0700967 @Override
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700968 public void showUntransformedConversation() {
969 super.showUntransformedConversation();
970 renderConversation(getMessageCursor());
971 }
972
973 @Override
Andy Huang46dfba62012-04-19 01:47:32 -0700974 public void onSuperCollapsedClick(SuperCollapsedBlockItem item) {
mindypf4fce122012-09-14 15:55:33 -0700975 MessageCursor cursor = getMessageCursor();
976 if (cursor == null || !mViewsCreated) {
Andy Huang46dfba62012-04-19 01:47:32 -0700977 return;
978 }
979
mindypf4fce122012-09-14 15:55:33 -0700980 mTempBodiesHtml = renderCollapsedHeaders(cursor, item);
Andy Huang46dfba62012-04-19 01:47:32 -0700981 mWebView.loadUrl("javascript:replaceSuperCollapsedBlock(" + item.getStart() + ")");
982 }
983
Andy Huang47aa9c92012-07-31 15:37:21 -0700984 private void showNewMessageNotification(NewMessagesInfo info) {
Andrew Sapperstein821fa872013-08-21 21:57:39 -0700985 mNewMessageBar.setText(info.getNotificationText());
Andy Huang47aa9c92012-07-31 15:37:21 -0700986 mNewMessageBar.setVisibility(View.VISIBLE);
987 }
988
989 private void onNewMessageBarClick() {
990 mNewMessageBar.setVisibility(View.GONE);
991
mindypf4fce122012-09-14 15:55:33 -0700992 renderConversation(getMessageCursor()); // mCursor is already up-to-date
993 // per onLoadFinished()
Andy Huang5fbda022012-02-28 18:22:03 -0800994 }
995
Andy Huangadbf3e82012-10-13 13:30:19 -0700996 private static OverlayPosition[] parsePositions(final String[] topArray,
997 final String[] bottomArray) {
998 final int len = topArray.length;
999 final OverlayPosition[] positions = new OverlayPosition[len];
Andy Huangb5078b22012-03-05 19:52:29 -08001000 for (int i = 0; i < len; i++) {
Andy Huangadbf3e82012-10-13 13:30:19 -07001001 positions[i] = new OverlayPosition(
1002 Integer.parseInt(topArray[i]), Integer.parseInt(bottomArray[i]));
Andy Huangb5078b22012-03-05 19:52:29 -08001003 }
Andy Huangadbf3e82012-10-13 13:30:19 -07001004 return positions;
Andy Huangb5078b22012-03-05 19:52:29 -08001005 }
1006
Andrew Sapperstein9f957f32013-07-19 15:18:18 -07001007 protected Address getAddress(String rawFrom) {
Andy Huang543e7092013-04-22 11:44:56 -07001008 Address addr;
1009 synchronized (mAddressCache) {
1010 addr = mAddressCache.get(rawFrom);
1011 if (addr == null) {
1012 addr = Address.getEmailAddress(rawFrom);
1013 mAddressCache.put(rawFrom, addr);
1014 }
Andy Huang16174812012-08-16 16:40:35 -07001015 }
1016 return addr;
1017 }
1018
Andy Huang9d3fd922012-09-26 22:23:58 -07001019 private void ensureContentSizeChangeListener() {
1020 if (mWebViewSizeChangeListener == null) {
Andy Huangc1fb9a92013-02-11 13:09:12 -08001021 mWebViewSizeChangeListener = new ContentSizeChangeListener() {
Andy Huang9d3fd922012-09-26 22:23:58 -07001022 @Override
1023 public void onHeightChange(int h) {
1024 // When WebKit says the DOM height has changed, re-measure
1025 // bodies and re-position their headers.
1026 // This is separate from the typical JavaScript DOM change
1027 // listeners because cases like NARROW_COLUMNS text reflow do not trigger DOM
1028 // events.
1029 mWebView.loadUrl("javascript:measurePositions();");
1030 }
1031 };
1032 }
1033 mWebView.setContentSizeChangeListener(mWebViewSizeChangeListener);
1034 }
1035
Andrew Sapperstein9f957f32013-07-19 15:18:18 -07001036 public static boolean isOverviewMode(Account acct) {
Andy Huangccf67802013-03-15 14:31:57 -07001037 return acct.settings.isOverviewMode();
Andy Huangadbf3e82012-10-13 13:30:19 -07001038 }
1039
1040 private void setupOverviewMode() {
Andy Huang02f9d182012-11-28 22:38:02 -08001041 // for now, overview mode means use the built-in WebView zoom and disable custom scale
1042 // gesture handling
Andy Huangadbf3e82012-10-13 13:30:19 -07001043 final boolean overviewMode = isOverviewMode(mAccount);
1044 final WebSettings settings = mWebView.getSettings();
Andy Huang06def562012-10-14 00:19:11 -07001045 settings.setUseWideViewPort(overviewMode);
Andy Huangc1fb9a92013-02-11 13:09:12 -08001046
1047 final OnScaleGestureListener listener;
1048
Andy Huang57f354c2013-04-11 17:23:40 -07001049 settings.setSupportZoom(overviewMode);
1050 settings.setBuiltInZoomControls(overviewMode);
1051 if (overviewMode) {
1052 settings.setDisplayZoomControls(false);
Andy Huangadbf3e82012-10-13 13:30:19 -07001053 }
Andy Huang57f354c2013-04-11 17:23:40 -07001054 listener = ENABLE_CSS_ZOOM && !overviewMode ? new CssScaleInterceptor() : null;
Andy Huangc1fb9a92013-02-11 13:09:12 -08001055
Andy Huang57f354c2013-04-11 17:23:40 -07001056 mWebView.setOnScaleGestureListener(listener);
Andy Huangadbf3e82012-10-13 13:30:19 -07001057 }
1058
Andrew Sappersteinb1d184d2013-08-09 14:14:31 -07001059 public class ConversationWebViewClient extends AbstractConversationWebViewClient {
Andrew Sapperstein4ddda2f2013-06-10 11:15:38 -07001060 public ConversationWebViewClient(Account account) {
1061 super(account);
Andrew Sapperstein376294b2013-06-06 16:04:26 -07001062 }
1063
Andy Huang17a9cde2012-03-09 18:03:16 -08001064 @Override
1065 public void onPageFinished(WebView view, String url) {
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001066 // Ignore unsafe calls made after a fragment is detached from an activity.
1067 // This method needs to, for example, get at the loader manager, which needs
1068 // the fragment to be added.
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001069 if (!isAdded() || !mViewsCreated) {
Paul Westbrook006e13c2013-07-24 18:40:20 -07001070 LogUtils.d(LOG_TAG, "ignoring CVF.onPageFinished, url=%s fragment=%s", url,
Andy Huangb95da852012-07-18 14:16:58 -07001071 ConversationViewFragment.this);
1072 return;
1073 }
1074
Paul Westbrook006e13c2013-07-24 18:40:20 -07001075 LogUtils.d(LOG_TAG, "IN CVF.onPageFinished, url=%s fragment=%s wv=%s t=%sms", url,
Andy Huang30bcfe72012-10-18 18:09:03 -07001076 ConversationViewFragment.this, view,
Andy Huang63b3c672012-10-05 19:27:28 -07001077 (SystemClock.uptimeMillis() - mWebViewLoadStartMs));
Andy Huang632721e2012-04-11 16:57:26 -07001078
Andy Huang9d3fd922012-09-26 22:23:58 -07001079 ensureContentSizeChangeListener();
1080
mindyp3bcf1802012-09-09 11:17:00 -07001081 if (!mEnableContentReadySignal) {
Andy Huang7d4746e2012-10-17 17:03:17 -07001082 revealConversation();
mindyp3bcf1802012-09-09 11:17:00 -07001083 }
Andy Huang9d3fd922012-09-26 22:23:58 -07001084
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001085 final Set<String> emailAddresses = Sets.newHashSet();
Andy Huang543e7092013-04-22 11:44:56 -07001086 final List<Address> cacheCopy;
1087 synchronized (mAddressCache) {
1088 cacheCopy = ImmutableList.copyOf(mAddressCache.values());
1089 }
1090 for (Address addr : cacheCopy) {
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001091 emailAddresses.add(addr.getAddress());
Andy Huangb8331b42012-07-16 19:08:53 -07001092 }
Andrew Sapperstein4ddda2f2013-06-10 11:15:38 -07001093 final ContactLoaderCallbacks callbacks = getContactInfoSource();
1094 callbacks.setSenders(emailAddresses);
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001095 getLoaderManager().restartLoader(CONTACT_LOADER, Bundle.EMPTY, callbacks);
Andy Huang17a9cde2012-03-09 18:03:16 -08001096 }
1097
Andy Huangaf5d4e02012-03-19 19:02:12 -07001098 @Override
1099 public boolean shouldOverrideUrlLoading(WebView view, String url) {
Paul Westbrook542fec92012-09-18 14:47:51 -07001100 return mViewsCreated && super.shouldOverrideUrlLoading(view, url);
Andy Huangaf5d4e02012-03-19 19:02:12 -07001101 }
Andy Huang17a9cde2012-03-09 18:03:16 -08001102 }
1103
Andy Huangf70fc402012-02-17 15:37:42 -08001104 /**
1105 * NOTE: all public methods must be listed in the proguard flags so that they can be accessed
1106 * via reflection and not stripped.
1107 *
1108 */
1109 private class MailJsBridge {
1110
1111 @SuppressWarnings("unused")
Mindy Pereira974c9662012-09-14 10:02:08 -07001112 @JavascriptInterface
Andy Huangadbf3e82012-10-13 13:30:19 -07001113 public void onWebContentGeometryChange(final String[] overlayTopStrs,
1114 final String[] overlayBottomStrs) {
Andrew Sapperstein376294b2013-06-06 16:04:26 -07001115 getHandler().post(new FragmentRunnable("onWebContentGeometryChange",
1116 ConversationViewFragment.this) {
mindyp1b3cc472012-09-27 11:32:59 -07001117
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001118 @Override
1119 public void go() {
1120 try {
Andy Huang46dfba62012-04-19 01:47:32 -07001121 if (!mViewsCreated) {
mindyp1b3cc472012-09-27 11:32:59 -07001122 LogUtils.d(LOG_TAG, "ignoring webContentGeometryChange because views"
1123 + " are gone, %s", ConversationViewFragment.this);
Andy Huang46dfba62012-04-19 01:47:32 -07001124 return;
1125 }
Andy Huangadbf3e82012-10-13 13:30:19 -07001126 mConversationContainer.onGeometryChange(
1127 parsePositions(overlayTopStrs, overlayBottomStrs));
mindyp1b3cc472012-09-27 11:32:59 -07001128 if (mDiff != 0) {
1129 // SCROLL!
1130 int scale = (int) (mWebView.getScale() / mWebView.getInitialScale());
1131 if (scale > 1) {
1132 mWebView.scrollBy(0, (mDiff * (scale - 1)));
1133 }
1134 mDiff = 0;
1135 }
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001136 } catch (Throwable t) {
1137 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onWebContentGeometryChange");
Andy Huang46dfba62012-04-19 01:47:32 -07001138 }
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001139 }
1140 });
Andy Huang46dfba62012-04-19 01:47:32 -07001141 }
1142
1143 @SuppressWarnings("unused")
Mindy Pereira974c9662012-09-14 10:02:08 -07001144 @JavascriptInterface
Andy Huang46dfba62012-04-19 01:47:32 -07001145 public String getTempMessageBodies() {
1146 try {
1147 if (!mViewsCreated) {
1148 return "";
Andy Huangf70fc402012-02-17 15:37:42 -08001149 }
Andy Huang46dfba62012-04-19 01:47:32 -07001150
1151 final String s = mTempBodiesHtml;
1152 mTempBodiesHtml = null;
1153 return s;
1154 } catch (Throwable t) {
1155 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getTempMessageBodies");
1156 return "";
1157 }
Andy Huangf70fc402012-02-17 15:37:42 -08001158 }
1159
Andy Huang014ea4c2012-09-25 14:50:54 -07001160 @SuppressWarnings("unused")
1161 @JavascriptInterface
1162 public String getMessageBody(String domId) {
1163 try {
1164 final MessageCursor cursor = getMessageCursor();
1165 if (!mViewsCreated || cursor == null) {
1166 return "";
1167 }
1168
1169 int pos = -1;
1170 while (cursor.moveToPosition(++pos)) {
1171 final ConversationMessage msg = cursor.getMessage();
1172 if (TextUtils.equals(domId, mTemplates.getMessageDomId(msg))) {
1173 return msg.getBodyAsHtml();
1174 }
1175 }
1176
1177 return "";
1178
1179 } catch (Throwable t) {
1180 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getMessageBody");
1181 return "";
1182 }
1183 }
1184
mindyp3bcf1802012-09-09 11:17:00 -07001185 @SuppressWarnings("unused")
Mindy Pereira974c9662012-09-14 10:02:08 -07001186 @JavascriptInterface
Andy Huang543e7092013-04-22 11:44:56 -07001187 public String getMessageSender(String domId) {
1188 try {
1189 final MessageCursor cursor = getMessageCursor();
1190 if (!mViewsCreated || cursor == null) {
1191 return "";
1192 }
1193
1194 int pos = -1;
1195 while (cursor.moveToPosition(++pos)) {
1196 final ConversationMessage msg = cursor.getMessage();
1197 if (TextUtils.equals(domId, mTemplates.getMessageDomId(msg))) {
1198 return getAddress(msg.getFrom()).getAddress();
1199 }
1200 }
1201
1202 return "";
1203
1204 } catch (Throwable t) {
1205 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getMessageSender");
1206 return "";
1207 }
1208 }
1209
1210 @SuppressWarnings("unused")
1211 @JavascriptInterface
mindyp3bcf1802012-09-09 11:17:00 -07001212 public void onContentReady() {
Andrew Sapperstein376294b2013-06-06 16:04:26 -07001213 getHandler().post(new FragmentRunnable("onContentReady",
1214 ConversationViewFragment.this) {
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001215 @Override
1216 public void go() {
1217 try {
Andy Huang63b3c672012-10-05 19:27:28 -07001218 if (mWebViewLoadStartMs != 0) {
1219 LogUtils.i(LOG_TAG, "IN CVF.onContentReady, f=%s vis=%s t=%sms",
1220 ConversationViewFragment.this,
1221 isUserVisible(),
1222 (SystemClock.uptimeMillis() - mWebViewLoadStartMs));
1223 }
Andy Huang7d4746e2012-10-17 17:03:17 -07001224 revealConversation();
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001225 } catch (Throwable t) {
1226 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onContentReady");
1227 // Still try to show the conversation.
1228 revealConversation();
mindyp3bcf1802012-09-09 11:17:00 -07001229 }
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001230 }
1231 });
mindyp3bcf1802012-09-09 11:17:00 -07001232 }
Andy Huange964eee2012-10-02 19:24:58 -07001233
1234 @SuppressWarnings("unused")
1235 @JavascriptInterface
1236 public float getScrollYPercent() {
1237 try {
1238 return mWebViewYPercent;
1239 } catch (Throwable t) {
1240 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getScrollYPercent");
1241 return 0f;
1242 }
1243 }
Andy Huang05c70c82013-03-14 15:15:50 -07001244
1245 @SuppressWarnings("unused")
1246 @JavascriptInterface
1247 public void onMessageTransform(String messageDomId, String transformText) {
Andrew Sappersteinae92e152013-05-03 13:55:18 -07001248 try {
1249 LogUtils.i(LOG_TAG, "TRANSFORM: (%s) %s", messageDomId, transformText);
1250 mMessageTransforms.put(messageDomId, transformText);
1251 onConversationTransformed();
1252 } catch (Throwable t) {
1253 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onMessageTransform");
1254 return;
1255 }
Andy Huang05c70c82013-03-14 15:15:50 -07001256 }
Andy Huangf70fc402012-02-17 15:37:42 -08001257 }
1258
Andy Huang47aa9c92012-07-31 15:37:21 -07001259 private class NewMessagesInfo {
1260 int count;
Andy Huang06c03622012-10-22 18:59:45 -07001261 int countFromSelf;
Andy Huang47aa9c92012-07-31 15:37:21 -07001262 String senderAddress;
1263
1264 /**
1265 * Return the display text for the new message notification overlay. It will be formatted
1266 * appropriately for a single new message vs. multiple new messages.
1267 *
1268 * @return display text
1269 */
1270 public String getNotificationText() {
mindypad0c30d2012-09-25 12:09:13 -07001271 Resources res = getResources();
Andy Huang47aa9c92012-07-31 15:37:21 -07001272 if (count > 1) {
mindypad0c30d2012-09-25 12:09:13 -07001273 return res.getString(R.string.new_incoming_messages_many, count);
Andy Huang47aa9c92012-07-31 15:37:21 -07001274 } else {
Andy Huang16174812012-08-16 16:40:35 -07001275 final Address addr = getAddress(senderAddress);
mindypad0c30d2012-09-25 12:09:13 -07001276 return res.getString(R.string.new_incoming_messages_one,
1277 TextUtils.isEmpty(addr.getName()) ? addr.getAddress() : addr.getName());
Andy Huang47aa9c92012-07-31 15:37:21 -07001278 }
Andy Huang47aa9c92012-07-31 15:37:21 -07001279 }
1280 }
1281
mindypf4fce122012-09-14 15:55:33 -07001282 @Override
Paul Westbrookc42ad5e2013-05-09 16:52:15 -07001283 public void onMessageCursorLoadFinished(Loader<ObjectCursor<ConversationMessage>> loader,
1284 MessageCursor newCursor, MessageCursor oldCursor) {
mindypf4fce122012-09-14 15:55:33 -07001285 /*
1286 * what kind of changes affect the MessageCursor? 1. new message(s) 2.
1287 * read/unread state change 3. deleted message, either regular or draft
1288 * 4. updated message, either from self or from others, updated in
1289 * content or state or sender 5. star/unstar of message (technically
1290 * similar to #1) 6. other label change Use MessageCursor.hashCode() to
1291 * sort out interesting vs. no-op cursor updates.
1292 */
Andy Huangb8331b42012-07-16 19:08:53 -07001293
Andy Huang233d4352012-10-18 14:00:24 -07001294 if (oldCursor != null && !oldCursor.isClosed()) {
Andy Huang014ea4c2012-09-25 14:50:54 -07001295 final NewMessagesInfo info = getNewIncomingMessagesInfo(newCursor);
Andy Huangb8331b42012-07-16 19:08:53 -07001296
Andy Huang014ea4c2012-09-25 14:50:54 -07001297 if (info.count > 0) {
1298 // don't immediately render new incoming messages from other
1299 // senders
1300 // (to avoid a new message from losing the user's focus)
1301 LogUtils.i(LOG_TAG, "CONV RENDER: conversation updated"
Andy Huang9d3fd922012-09-26 22:23:58 -07001302 + ", holding cursor for new incoming message (%s)", this);
Andy Huang014ea4c2012-09-25 14:50:54 -07001303 showNewMessageNotification(info);
1304 return;
1305 }
1306
Andy Huang06c03622012-10-22 18:59:45 -07001307 final int oldState = oldCursor.getStateHashCode();
1308 final boolean changed = newCursor.getStateHashCode() != oldState;
Andy Huang233d4352012-10-18 14:00:24 -07001309
Andy Huang014ea4c2012-09-25 14:50:54 -07001310 if (!changed) {
1311 final boolean processedInPlace = processInPlaceUpdates(newCursor, oldCursor);
1312 if (processedInPlace) {
Andy Huang9d3fd922012-09-26 22:23:58 -07001313 LogUtils.i(LOG_TAG, "CONV RENDER: processed update(s) in place (%s)", this);
Andy Huang1ee96b22012-08-24 20:19:53 -07001314 } else {
mindypf4fce122012-09-14 15:55:33 -07001315 LogUtils.i(LOG_TAG, "CONV RENDER: uninteresting update"
Andy Huang9d3fd922012-09-26 22:23:58 -07001316 + ", ignoring this conversation update (%s)", this);
Andy Huang1ee96b22012-08-24 20:19:53 -07001317 }
Andy Huangb8331b42012-07-16 19:08:53 -07001318 return;
Andy Huang06c03622012-10-22 18:59:45 -07001319 } else if (info.countFromSelf == 1) {
1320 // Special-case the very common case of a new cursor that is the same as the old
1321 // one, except that there is a new message from yourself. This happens upon send.
1322 final boolean sameExceptNewLast = newCursor.getStateHashCode(1) == oldState;
1323 if (sameExceptNewLast) {
1324 LogUtils.i(LOG_TAG, "CONV RENDER: update is a single new message from self"
1325 + " (%s)", this);
1326 newCursor.moveToLast();
1327 processNewOutgoingMessage(newCursor.getMessage());
1328 return;
1329 }
Andy Huangb8331b42012-07-16 19:08:53 -07001330 }
Andy Huang6766b6e2012-09-28 12:43:52 -07001331 // cursors are different, and not due to an incoming message. fall
1332 // through and render.
1333 LogUtils.i(LOG_TAG, "CONV RENDER: conversation updated"
1334 + ", but not due to incoming message. rendering. (%s)", this);
Andy Huang06c03622012-10-22 18:59:45 -07001335
1336 if (DEBUG_DUMP_CURSOR_CONTENTS) {
1337 LogUtils.i(LOG_TAG, "old cursor: %s", oldCursor.getDebugDump());
1338 LogUtils.i(LOG_TAG, "new cursor: %s", newCursor.getDebugDump());
1339 }
Andy Huang6766b6e2012-09-28 12:43:52 -07001340 } else {
1341 LogUtils.i(LOG_TAG, "CONV RENDER: initial render. (%s)", this);
Andy Huang243c2362013-03-01 17:50:35 -08001342 timerMark("message cursor load finished");
Andy Huangb8331b42012-07-16 19:08:53 -07001343 }
1344
Andrew Sapperstein606dbd72013-07-30 19:14:23 -07001345 renderContent(newCursor);
1346 }
1347
1348 protected void renderContent(MessageCursor messageCursor) {
Mark Wei4071c2f2012-09-26 14:38:38 -07001349 // if layout hasn't happened, delay render
1350 // This is needed in addition to the showConversation() delay to speed
1351 // up rotation and restoration.
1352 if (mConversationContainer.getWidth() == 0) {
1353 mNeedRender = true;
1354 mConversationContainer.addOnLayoutChangeListener(this);
1355 } else {
Andrew Sapperstein606dbd72013-07-30 19:14:23 -07001356 renderConversation(messageCursor);
Mark Wei4071c2f2012-09-26 14:38:38 -07001357 }
Andy Huangb8331b42012-07-16 19:08:53 -07001358 }
1359
mindypf4fce122012-09-14 15:55:33 -07001360 private NewMessagesInfo getNewIncomingMessagesInfo(MessageCursor newCursor) {
1361 final NewMessagesInfo info = new NewMessagesInfo();
Andy Huangb8331b42012-07-16 19:08:53 -07001362
mindypf4fce122012-09-14 15:55:33 -07001363 int pos = -1;
1364 while (newCursor.moveToPosition(++pos)) {
1365 final Message m = newCursor.getMessage();
1366 if (!mViewState.contains(m)) {
1367 LogUtils.i(LOG_TAG, "conversation diff: found new msg: %s", m.uri);
Andy Huangb8331b42012-07-16 19:08:53 -07001368
Scott Kennedy8960f0a2012-11-07 15:35:50 -08001369 final Address from = getAddress(m.getFrom());
mindypf4fce122012-09-14 15:55:33 -07001370 // distinguish ours from theirs
1371 // new messages from the account owner should not trigger a
1372 // notification
1373 if (mAccount.ownsFromAddress(from.getAddress())) {
1374 LogUtils.i(LOG_TAG, "found message from self: %s", m.uri);
Andy Huang06c03622012-10-22 18:59:45 -07001375 info.countFromSelf++;
mindypf4fce122012-09-14 15:55:33 -07001376 continue;
1377 }
Andy Huangb8331b42012-07-16 19:08:53 -07001378
mindypf4fce122012-09-14 15:55:33 -07001379 info.count++;
Scott Kennedy8960f0a2012-11-07 15:35:50 -08001380 info.senderAddress = m.getFrom();
Andy Huangb8331b42012-07-16 19:08:53 -07001381 }
Andy Huangb8331b42012-07-16 19:08:53 -07001382 }
mindypf4fce122012-09-14 15:55:33 -07001383 return info;
Andy Huangb8331b42012-07-16 19:08:53 -07001384 }
1385
Andy Huang014ea4c2012-09-25 14:50:54 -07001386 private boolean processInPlaceUpdates(MessageCursor newCursor, MessageCursor oldCursor) {
1387 final Set<String> idsOfChangedBodies = Sets.newHashSet();
Andy Huang6b3d0d92012-10-30 15:46:48 -07001388 final List<Integer> changedOverlayPositions = Lists.newArrayList();
1389
Andy Huang014ea4c2012-09-25 14:50:54 -07001390 boolean changed = false;
1391
1392 int pos = 0;
1393 while (true) {
1394 if (!newCursor.moveToPosition(pos) || !oldCursor.moveToPosition(pos)) {
1395 break;
1396 }
1397
1398 final ConversationMessage newMsg = newCursor.getMessage();
1399 final ConversationMessage oldMsg = oldCursor.getMessage();
1400
Scott Kennedy8960f0a2012-11-07 15:35:50 -08001401 if (!TextUtils.equals(newMsg.getFrom(), oldMsg.getFrom()) ||
Andy Huang2a1e8e32012-10-23 18:54:57 -07001402 newMsg.isSending != oldMsg.isSending) {
Andy Huang6b3d0d92012-10-30 15:46:48 -07001403 mAdapter.updateItemsForMessage(newMsg, changedOverlayPositions);
Andy Huang2a1e8e32012-10-23 18:54:57 -07001404 LogUtils.i(LOG_TAG, "msg #%d (%d): detected from/sending change. isSending=%s",
1405 pos, newMsg.id, newMsg.isSending);
Andy Huang014ea4c2012-09-25 14:50:54 -07001406 }
1407
1408 // update changed message bodies in-place
1409 if (!TextUtils.equals(newMsg.bodyHtml, oldMsg.bodyHtml) ||
1410 !TextUtils.equals(newMsg.bodyText, oldMsg.bodyText)) {
1411 // maybe just set a flag to notify JS to re-request changed bodies
1412 idsOfChangedBodies.add('"' + mTemplates.getMessageDomId(newMsg) + '"');
1413 LogUtils.i(LOG_TAG, "msg #%d (%d): detected body change", pos, newMsg.id);
1414 }
1415
1416 pos++;
1417 }
1418
Andy Huang6b3d0d92012-10-30 15:46:48 -07001419
1420 if (!changedOverlayPositions.isEmpty()) {
Andy Huang06c03622012-10-22 18:59:45 -07001421 // notify once after the entire adapter is updated
Andy Huang6b3d0d92012-10-30 15:46:48 -07001422 mConversationContainer.onOverlayModelUpdate(changedOverlayPositions);
1423 changed = true;
Andy Huang06c03622012-10-22 18:59:45 -07001424 }
1425
Andy Huang014ea4c2012-09-25 14:50:54 -07001426 if (!idsOfChangedBodies.isEmpty()) {
1427 mWebView.loadUrl(String.format("javascript:replaceMessageBodies([%s]);",
1428 TextUtils.join(",", idsOfChangedBodies)));
1429 changed = true;
1430 }
1431
1432 return changed;
1433 }
1434
Andy Huang06c03622012-10-22 18:59:45 -07001435 private void processNewOutgoingMessage(ConversationMessage msg) {
Andrew Sappersteincee3c902013-07-31 10:52:02 -07001436 // make the last border no longer be the border
1437 ((BorderItem) mAdapter.getItem(mAdapter.getCount() - 1)).setIsLastBorder(false);
1438
Andy Huang06c03622012-10-22 18:59:45 -07001439 mTemplates.reset();
1440 // this method will add some items to mAdapter, but we deliberately want to avoid notifying
1441 // adapter listeners (i.e. ConversationContainer) until onWebContentGeometryChange is next
1442 // called, to prevent N+1 headers rendering with N message bodies.
Andrew Sappersteincee3c902013-07-31 10:52:02 -07001443
1444 // We can just call previousCollapsed false here since the border
1445 // above the message we're about to render should always show
1446 // (which it also will since the message being render is expanded).
1447 renderMessage(msg, false /* previousCollapsed */, true /* expanded */,
1448 msg.alwaysShowImages, false /* renderBorder */, false /* firstBorder */);
1449 renderBorder(true /* contiguous */, true /* expanded */,
1450 false /* firstBorder */, true /* lastBorder */);
Andy Huang06c03622012-10-22 18:59:45 -07001451 mTempBodiesHtml = mTemplates.emit();
1452
1453 mViewState.setExpansionState(msg, ExpansionState.EXPANDED);
1454 // FIXME: should the provider set this as initial state?
1455 mViewState.setReadState(msg, false /* read */);
1456
Andy Huang91d782a2012-10-25 12:37:29 -07001457 // From now until the updated spacer geometry is returned, the adapter items are mismatched
1458 // with the existing spacers. Do not let them layout.
1459 mConversationContainer.invalidateSpacerGeometry();
1460
Andy Huang06c03622012-10-22 18:59:45 -07001461 mWebView.loadUrl("javascript:appendMessageHtml();");
1462 }
1463
Paul Westbrookcebcc642012-08-08 10:06:04 -07001464 private class SetCookieTask extends AsyncTask<Void, Void, Void> {
1465 final String mUri;
Paul Westbrookb8361c92012-09-27 10:57:14 -07001466 final Uri mAccountCookieQueryUri;
1467 final ContentResolver mResolver;
Paul Westbrookcebcc642012-08-08 10:06:04 -07001468
Paul Westbrookb8361c92012-09-27 10:57:14 -07001469 SetCookieTask(Context context, Uri baseUri, Uri accountCookieQueryUri) {
1470 mUri = baseUri.toString();
1471 mAccountCookieQueryUri = accountCookieQueryUri;
1472 mResolver = context.getContentResolver();
Paul Westbrookcebcc642012-08-08 10:06:04 -07001473 }
1474
1475 @Override
1476 public Void doInBackground(Void... args) {
Paul Westbrookb8361c92012-09-27 10:57:14 -07001477 // First query for the coookie string from the UI provider
1478 final Cursor cookieCursor = mResolver.query(mAccountCookieQueryUri,
1479 UIProvider.ACCOUNT_COOKIE_PROJECTION, null, null, null);
1480 if (cookieCursor == null) {
1481 return null;
1482 }
1483
1484 try {
1485 if (cookieCursor.moveToFirst()) {
1486 final String cookie = cookieCursor.getString(
1487 cookieCursor.getColumnIndex(UIProvider.AccountCookieColumns.COOKIE));
1488
1489 if (cookie != null) {
1490 final CookieSyncManager csm =
1491 CookieSyncManager.createInstance(getContext());
1492 CookieManager.getInstance().setCookie(mUri, cookie);
1493 csm.sync();
1494 }
1495 }
1496
1497 } finally {
1498 cookieCursor.close();
1499 }
1500
1501
Paul Westbrookcebcc642012-08-08 10:06:04 -07001502 return null;
1503 }
1504 }
mindyp36280f32012-09-09 16:11:23 -07001505
mindyp26d4d2d2012-09-18 17:30:32 -07001506 @Override
mindyp36280f32012-09-09 16:11:23 -07001507 public void onConversationUpdated(Conversation conv) {
1508 final ConversationViewHeader headerView = (ConversationViewHeader) mConversationContainer
1509 .findViewById(R.id.conversation_header);
mindypb2b98ba2012-09-24 14:13:58 -07001510 mConversation = conv;
mindyp9e0b2362012-09-09 16:31:21 -07001511 if (headerView != null) {
1512 headerView.onConversationUpdated(conv);
Vikram Aggarwal51ad9042013-01-17 13:18:09 -08001513 headerView.setSubject(conv.subject);
mindyp9e0b2362012-09-09 16:31:21 -07001514 }
mindyp36280f32012-09-09 16:11:23 -07001515 }
Mark Wei4071c2f2012-09-26 14:38:38 -07001516
1517 @Override
1518 public void onLayoutChange(View v, int left, int top, int right,
1519 int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
1520 boolean sizeChanged = mNeedRender
1521 && mConversationContainer.getWidth() != 0;
1522 if (sizeChanged) {
1523 mNeedRender = false;
1524 mConversationContainer.removeOnLayoutChangeListener(this);
1525 renderConversation(getMessageCursor());
1526 }
1527 }
mindyp1b3cc472012-09-27 11:32:59 -07001528
1529 @Override
1530 public void setMessageDetailsExpanded(MessageHeaderItem i, boolean expanded,
1531 int heightBefore) {
1532 mDiff = (expanded ? 1 : -1) * Math.abs(i.getHeight() - heightBefore);
1533 }
Andy Huang02f9d182012-11-28 22:38:02 -08001534
Andy Huangc1fb9a92013-02-11 13:09:12 -08001535 private class CssScaleInterceptor implements OnScaleGestureListener {
Andy Huang02f9d182012-11-28 22:38:02 -08001536
1537 private float getFocusXWebPx(ScaleGestureDetector detector) {
1538 return (detector.getFocusX() - mSideMarginPx) / mWebView.getInitialScale();
1539 }
1540
1541 private float getFocusYWebPx(ScaleGestureDetector detector) {
1542 return detector.getFocusY() / mWebView.getInitialScale();
1543 }
1544
1545 @Override
1546 public boolean onScale(ScaleGestureDetector detector) {
1547 mWebView.loadUrl(String.format("javascript:onScale(%s, %s, %s);",
1548 detector.getScaleFactor(), getFocusXWebPx(detector),
1549 getFocusYWebPx(detector)));
1550 return false;
1551 }
1552
1553 @Override
1554 public boolean onScaleBegin(ScaleGestureDetector detector) {
1555 mWebView.loadUrl(String.format("javascript:onScaleBegin(%s, %s);",
1556 getFocusXWebPx(detector), getFocusYWebPx(detector)));
1557 return true;
1558 }
1559
1560 @Override
1561 public void onScaleEnd(ScaleGestureDetector detector) {
1562 mWebView.loadUrl(String.format("javascript:onScaleEnd(%s, %s);",
1563 getFocusXWebPx(detector), getFocusYWebPx(detector)));
1564 }
1565
1566 }
Mindy Pereira9b875682012-02-15 18:10:54 -08001567}