blob: f5a0cdad71185b168a23cfe747adeb6a5ad067bb [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;
Andrew Sapperstein3af481c2013-10-30 10:29:38 -070030import android.support.v4.text.BidiFormatter;
Andrew Sapperstein8ec43e82013-12-17 18:27:55 -080031import android.support.v4.util.ArrayMap;
Andy Huang47aa9c92012-07-31 15:37:21 -070032import android.text.TextUtils;
Mindy Pereira9b875682012-02-15 18:10:54 -080033import android.view.LayoutInflater;
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
Tony Mantler821e5782014-01-06 15:33:43 -080046import com.android.emailcommon.mail.Address;
Andy Huang59e0b182012-08-14 14:32:23 -070047import com.android.mail.FormattedDateBuilder;
Mindy Pereira9b875682012-02-15 18:10:54 -080048import com.android.mail.R;
Andy Huange6c9fb62013-11-15 09:56:20 -080049import com.android.mail.analytics.Analytics;
Jin Cao72953f22014-04-15 18:23:37 -070050import com.android.mail.analytics.AnalyticsTimer;
Andy Huang5ff63742012-03-16 20:30:23 -070051import com.android.mail.browse.ConversationContainer;
Andy Huangadbf3e82012-10-13 13:30:19 -070052import com.android.mail.browse.ConversationContainer.OverlayPosition;
Andrew Sapperstein735a22a2014-07-11 03:57:42 -070053import com.android.mail.browse.ConversationFooterView.ConversationFooterCallbacks;
Andrew Sapperstein8812d3c2013-06-04 17:06:41 -070054import com.android.mail.browse.ConversationMessage;
Andy Huang46dfba62012-04-19 01:47:32 -070055import com.android.mail.browse.ConversationOverlayItem;
Andy Huang7bdc3752012-03-25 17:18:19 -070056import com.android.mail.browse.ConversationViewAdapter;
Andrew Sappersteine2a30e12014-07-02 22:36:56 -070057import com.android.mail.browse.ConversationViewAdapter.ConversationFooterItem;
Andy Huang46dfba62012-04-19 01:47:32 -070058import com.android.mail.browse.ConversationViewAdapter.MessageFooterItem;
Andy Huang7bdc3752012-03-25 17:18:19 -070059import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem;
Andy Huang46dfba62012-04-19 01:47:32 -070060import com.android.mail.browse.ConversationViewAdapter.SuperCollapsedBlockItem;
Andy Huang5ff63742012-03-16 20:30:23 -070061import com.android.mail.browse.ConversationViewHeader;
62import com.android.mail.browse.ConversationWebView;
Andrew Sapperstein8ec43e82013-12-17 18:27:55 -080063import com.android.mail.browse.InlineAttachmentViewIntentBuilderCreator;
64import com.android.mail.browse.InlineAttachmentViewIntentBuilderCreatorHolder;
Andy Huangc1fb9a92013-02-11 13:09:12 -080065import com.android.mail.browse.MailWebView.ContentSizeChangeListener;
Andy Huang7bdc3752012-03-25 17:18:19 -070066import com.android.mail.browse.MessageCursor;
Andrew Sapperstein381c3222014-04-20 12:23:57 -070067import com.android.mail.browse.MessageFooterView;
Andy Huang59e0b182012-08-14 14:32:23 -070068import com.android.mail.browse.MessageHeaderView;
Andy Huangadbf3e82012-10-13 13:30:19 -070069import com.android.mail.browse.ScrollIndicatorsView;
Andy Huang46dfba62012-04-19 01:47:32 -070070import com.android.mail.browse.SuperCollapsedBlock;
Andy Huang0b7ed6f2012-07-25 19:23:26 -070071import com.android.mail.browse.WebViewContextMenu;
Paul Westbrookc42ad5e2013-05-09 16:52:15 -070072import com.android.mail.content.ObjectCursor;
Andrew Sapperstein562c5ba2013-10-09 18:31:50 -070073import com.android.mail.print.PrintUtils;
Mindy Pereira9b875682012-02-15 18:10:54 -080074import com.android.mail.providers.Account;
75import com.android.mail.providers.Conversation;
Andy Huangf70fc402012-02-17 15:37:42 -080076import com.android.mail.providers.Message;
Alice Yangf323c042013-10-30 00:15:02 -070077import com.android.mail.providers.Settings;
Paul Westbrookb8361c92012-09-27 10:57:14 -070078import com.android.mail.providers.UIProvider;
Andy Huangcd5c5ee2012-08-12 19:03:51 -070079import com.android.mail.ui.ConversationViewState.ExpansionState;
Andrew Sapperstein376294b2013-06-06 16:04:26 -070080import com.android.mail.utils.ConversationViewUtils;
Paul Westbrookb334c902012-06-25 11:42:46 -070081import com.android.mail.utils.LogTag;
Mindy Pereira9b875682012-02-15 18:10:54 -080082import com.android.mail.utils.LogUtils;
Andy Huang2e9acfe2012-03-15 22:39:36 -070083import com.android.mail.utils.Utils;
Andy Huang543e7092013-04-22 11:44:56 -070084import com.google.common.collect.ImmutableList;
Andy Huang46dfba62012-04-19 01:47:32 -070085import com.google.common.collect.Lists;
Andy Huang05c70c82013-03-14 15:15:50 -070086import com.google.common.collect.Maps;
Andy Huangb8331b42012-07-16 19:08:53 -070087import com.google.common.collect.Sets;
Andy Huang65fe28f2012-04-06 18:08:53 -070088
Scott Kennedyeb9a4bd2012-11-12 10:33:04 -080089import java.util.ArrayList;
Andy Huang46dfba62012-04-19 01:47:32 -070090import java.util.List;
Andy Huang05c70c82013-03-14 15:15:50 -070091import java.util.Map;
Andy Huangb8331b42012-07-16 19:08:53 -070092import java.util.Set;
Mindy Pereira9b875682012-02-15 18:10:54 -080093
94/**
95 * The conversation view UI component.
96 */
Andrew Sapperstein9f957f32013-07-19 15:18:18 -070097public class ConversationViewFragment extends AbstractConversationViewFragment implements
Andrew Sapperstein4ddda2f2013-06-10 11:15:38 -070098 SuperCollapsedBlock.OnClickListener, OnLayoutChangeListener,
Andrew Sapperstein735a22a2014-07-11 03:57:42 -070099 MessageHeaderView.MessageHeaderViewCallbacks, MessageFooterView.MessageFooterCallbacks,
100 WebViewContextMenu.Callbacks, ConversationFooterCallbacks {
Mindy Pereira8e915722012-02-16 14:42:56 -0800101
Paul Westbrookb334c902012-06-25 11:42:46 -0700102 private static final String LOG_TAG = LogTag.getLogTag();
Andy Huang632721e2012-04-11 16:57:26 -0700103 public static final String LAYOUT_TAG = "ConvLayout";
Mindy Pereira9b875682012-02-15 18:10:54 -0800104
Andy Huang9d3fd922012-09-26 22:23:58 -0700105 /**
mindyp1b3cc472012-09-27 11:32:59 -0700106 * Difference in the height of the message header whose details have been expanded/collapsed
107 */
108 private int mDiff = 0;
109
110 /**
Andy Huang9d3fd922012-09-26 22:23:58 -0700111 * Default value for {@link #mLoadWaitReason}. Conversation load will happen immediately.
112 */
113 private final int LOAD_NOW = 0;
114 /**
115 * Value for {@link #mLoadWaitReason} that means we are offscreen and waiting for the visible
116 * conversation to finish loading before beginning our load.
117 * <p>
118 * When this value is set, the fragment should register with {@link ConversationListCallbacks}
119 * to know when the visible conversation is loaded. When it is unset, it should unregister.
120 */
121 private final int LOAD_WAIT_FOR_INITIAL_CONVERSATION = 1;
122 /**
123 * Value for {@link #mLoadWaitReason} used when a conversation is too heavyweight to load at
124 * all when not visible (e.g. requires network fetch, or too complex). Conversation load will
125 * wait until this fragment is visible.
126 */
127 private final int LOAD_WAIT_UNTIL_VISIBLE = 2;
mindyp3bcf1802012-09-09 11:17:00 -0700128
Andrew Sapperstein9f957f32013-07-19 15:18:18 -0700129 protected ConversationContainer mConversationContainer;
Mindy Pereira9b875682012-02-15 18:10:54 -0800130
Andrew Sapperstein9f957f32013-07-19 15:18:18 -0700131 protected ConversationWebView mWebView;
Mindy Pereira9b875682012-02-15 18:10:54 -0800132
Andrew Sappersteina7fa9bf2014-03-29 13:33:04 -0700133 private ConversationViewProgressController mProgressController;
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700134
Andrew Sapperstein821fa872013-08-21 21:57:39 -0700135 private Button mNewMessageBar;
Andy Huang47aa9c92012-07-31 15:37:21 -0700136
Andrew Sapperstein9f957f32013-07-19 15:18:18 -0700137 protected HtmlConversationTemplates mTemplates;
Andy Huangf70fc402012-02-17 15:37:42 -0800138
Andy Huangf70fc402012-02-17 15:37:42 -0800139 private final MailJsBridge mJsBridge = new MailJsBridge();
140
Andrew Sapperstein9f957f32013-07-19 15:18:18 -0700141 protected ConversationViewAdapter mAdapter;
Andy Huang51067132012-03-12 20:08:19 -0700142
Andrew Sappersteinb1d184d2013-08-09 14:14:31 -0700143 protected boolean mViewsCreated;
Mark Wei4071c2f2012-09-26 14:38:38 -0700144 // True if we attempted to render before the views were laid out
145 // We will render immediately once layout is done
146 private boolean mNeedRender;
Andy Huang51067132012-03-12 20:08:19 -0700147
Andy Huang46dfba62012-04-19 01:47:32 -0700148 /**
149 * Temporary string containing the message bodies of the messages within a super-collapsed
150 * block, for one-time use during block expansion. We cannot easily pass the body HTML
151 * into JS without problematic escaping, so hold onto it momentarily and signal JS to fetch it
152 * using {@link MailJsBridge}.
153 */
154 private String mTempBodiesHtml;
155
Andy Huang632721e2012-04-11 16:57:26 -0700156 private int mMaxAutoLoadMessages;
157
Andrew Sapperstein9f957f32013-07-19 15:18:18 -0700158 protected int mSideMarginPx;
Andy Huang02f9d182012-11-28 22:38:02 -0800159
Andy Huang9d3fd922012-09-26 22:23:58 -0700160 /**
161 * If this conversation fragment is not visible, and it's inappropriate to load up front,
162 * this is the reason we are waiting. This flag should be cleared once it's okay to load
163 * the conversation.
164 */
165 private int mLoadWaitReason = LOAD_NOW;
Andy Huang632721e2012-04-11 16:57:26 -0700166
mindyp3bcf1802012-09-09 11:17:00 -0700167 private boolean mEnableContentReadySignal;
Andy Huang28b7aee2012-08-20 20:27:32 -0700168
mindypdde3f9f2012-09-10 17:35:35 -0700169 private ContentSizeChangeListener mWebViewSizeChangeListener;
170
Andy Huange964eee2012-10-02 19:24:58 -0700171 private float mWebViewYPercent;
172
173 /**
174 * Has loadData been called on the WebView yet?
175 */
176 private boolean mWebViewLoadedData;
177
Andy Huang63b3c672012-10-05 19:27:28 -0700178 private long mWebViewLoadStartMs;
179
Andy Huang05c70c82013-03-14 15:15:50 -0700180 private final Map<String, String> mMessageTransforms = Maps.newHashMap();
181
Andy Huang9d3fd922012-09-26 22:23:58 -0700182 private final DataSetObserver mLoadedObserver = new DataSetObserver() {
183 @Override
184 public void onChanged() {
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700185 getHandler().post(new FragmentRunnable("delayedConversationLoad",
186 ConversationViewFragment.this) {
Andy Huang9d3fd922012-09-26 22:23:58 -0700187 @Override
188 public void go() {
189 LogUtils.d(LOG_TAG, "CVF load observer fired, this=%s",
190 ConversationViewFragment.this);
191 handleDelayedConversationLoad();
192 }
193 });
194 }
195 };
Andy Huangf70fc402012-02-17 15:37:42 -0800196
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700197 private final Runnable mOnProgressDismiss = new FragmentRunnable("onProgressDismiss", this) {
Andy Huang7d4746e2012-10-17 17:03:17 -0700198 @Override
199 public void go() {
Scott Kennedy58192e52013-05-08 16:35:57 -0700200 LogUtils.d(LOG_TAG, "onProgressDismiss go() - isUserVisible() = %b", isUserVisible());
Andy Huang7d4746e2012-10-17 17:03:17 -0700201 if (isUserVisible()) {
202 onConversationSeen();
203 }
Andy Huang30bcfe72012-10-18 18:09:03 -0700204 mWebView.onRenderComplete();
Andy Huang7d4746e2012-10-17 17:03:17 -0700205 }
206 };
207
Andy Huangbd544e32012-05-29 15:56:51 -0700208 private static final boolean DEBUG_DUMP_CONVERSATION_HTML = false;
Andy Huang47aa9c92012-07-31 15:37:21 -0700209 private static final boolean DISABLE_OFFSCREEN_LOADING = false;
Andy Huang06c03622012-10-22 18:59:45 -0700210 private static final boolean DEBUG_DUMP_CURSOR_CONTENTS = false;
Andy Huange964eee2012-10-02 19:24:58 -0700211
212 private static final String BUNDLE_KEY_WEBVIEW_Y_PERCENT =
213 ConversationViewFragment.class.getName() + "webview-y-percent";
Andy Huangbd544e32012-05-29 15:56:51 -0700214
Andrew Sapperstein2fd167d2014-01-28 10:07:38 -0800215 private BidiFormatter mBidiFormatter;
Andrew Sapperstein3af481c2013-10-30 10:29:38 -0700216
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800217 /**
Andrew Sapperstein8ec43e82013-12-17 18:27:55 -0800218 * Contains a mapping between inline image attachments and their local message id.
219 */
220 private Map<String, String> mUrlToMessageIdMap;
221
222 /**
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800223 * Constructor needs to be public to handle orientation changes and activity lifecycle events.
224 */
Paul Westbrookf0ea4842013-08-13 16:41:18 -0700225 public ConversationViewFragment() {}
Mindy Pereira9b875682012-02-15 18:10:54 -0800226
227 /**
228 * Creates a new instance of {@link ConversationViewFragment}, initialized
Andy Huang632721e2012-04-11 16:57:26 -0700229 * to display a conversation with other parameters inherited/copied from an existing bundle,
230 * typically one created using {@link #makeBasicArgs}.
231 */
232 public static ConversationViewFragment newInstance(Bundle existingArgs,
233 Conversation conversation) {
234 ConversationViewFragment f = new ConversationViewFragment();
235 Bundle args = new Bundle(existingArgs);
236 args.putParcelable(ARG_CONVERSATION, conversation);
237 f.setArguments(args);
238 return f;
239 }
240
mindypf4fce122012-09-14 15:55:33 -0700241 @Override
Andy Huangadbf3e82012-10-13 13:30:19 -0700242 public void onAccountChanged(Account newAccount, Account oldAccount) {
243 // if overview mode has changed, re-render completely (no need to also update headers)
244 if (isOverviewMode(newAccount) != isOverviewMode(oldAccount)) {
245 setupOverviewMode();
246 final MessageCursor c = getMessageCursor();
247 if (c != null) {
248 renderConversation(c);
249 } else {
250 // Null cursor means this fragment is either waiting to load or in the middle of
251 // loading. Either way, a future render will happen anyway, and the new setting
252 // will take effect when that happens.
253 }
254 return;
255 }
256
mindypf4fce122012-09-14 15:55:33 -0700257 // settings may have been updated; refresh views that are known to
258 // depend on settings
mindypf4fce122012-09-14 15:55:33 -0700259 mAdapter.notifyDataSetChanged();
Andy Huang632721e2012-04-11 16:57:26 -0700260 }
261
Mindy Pereira9b875682012-02-15 18:10:54 -0800262 @Override
263 public void onActivityCreated(Bundle savedInstanceState) {
Andy Huang9d3fd922012-09-26 22:23:58 -0700264 LogUtils.d(LOG_TAG, "IN CVF.onActivityCreated, this=%s visible=%s", this, isUserVisible());
Mindy Pereira9b875682012-02-15 18:10:54 -0800265 super.onActivityCreated(savedInstanceState);
Mark Wei1abfcaf2012-09-27 11:11:07 -0700266
267 if (mActivity == null || mActivity.isFinishing()) {
268 // Activity is finishing, just bail.
269 return;
270 }
271
mindypf4fce122012-09-14 15:55:33 -0700272 Context context = getContext();
273 mTemplates = new HtmlConversationTemplates(context);
Andy Huang59e0b182012-08-14 14:32:23 -0700274
mindypf4fce122012-09-14 15:55:33 -0700275 final FormattedDateBuilder dateBuilder = new FormattedDateBuilder(context);
Andy Huang59e0b182012-08-14 14:32:23 -0700276
Paul Westbrook8081df42012-09-10 15:43:36 -0700277 mAdapter = new ConversationViewAdapter(mActivity, this,
Andrew Sapperstein735a22a2014-07-11 03:57:42 -0700278 getLoaderManager(), this, this, getContactInfoSource(), this, this,
Andrew Sappersteineb58a092014-06-24 14:03:49 -0700279 getListController(), this, mAddressCache, dateBuilder, mBidiFormatter);
Andy Huang51067132012-03-12 20:08:19 -0700280 mConversationContainer.setOverlayAdapter(mAdapter);
281
Andy Huang59e0b182012-08-14 14:32:23 -0700282 // set up snap header (the adapter usually does this with the other ones)
Andrew Sapperstein85ea6182013-10-14 18:21:08 -0700283 mConversationContainer.getSnapHeader().initialize(
284 this, mAddressCache, this, getContactInfoSource(),
285 mActivity.getAccountController().getVeiledAddressMatcher());
Andy Huangc1fb9a92013-02-11 13:09:12 -0800286
Andrew Sapperstein90eccf92013-08-22 11:53:23 -0700287 final Resources resources = getResources();
288 mMaxAutoLoadMessages = resources.getInteger(R.integer.max_auto_load_messages);
Andy Huang632721e2012-04-11 16:57:26 -0700289
Andrew Sapperstein90eccf92013-08-22 11:53:23 -0700290 mSideMarginPx = resources.getDimensionPixelOffset(
Andy Huang02f9d182012-11-28 22:38:02 -0800291 R.dimen.conversation_message_content_margin_side);
292
Andrew Sapperstein8ec43e82013-12-17 18:27:55 -0800293 mUrlToMessageIdMap = new ArrayMap<String, String>();
294 final InlineAttachmentViewIntentBuilderCreator creator =
295 InlineAttachmentViewIntentBuilderCreatorHolder.
296 getInlineAttachmentViewIntentCreator();
Andy Huang3c6fd442014-03-24 19:56:46 -0700297 final WebViewContextMenu contextMenu = new WebViewContextMenu(getActivity(),
298 creator.createInlineAttachmentViewIntentBuilder(mAccount.getEmailAddress(),
299 mConversation != null ? mConversation.id : -1));
300 contextMenu.setCallbacks(this);
301 mWebView.setOnCreateContextMenuListener(contextMenu);
Andy Huang0b7ed6f2012-07-25 19:23:26 -0700302
Andy Huangadbf3e82012-10-13 13:30:19 -0700303 // set this up here instead of onCreateView to ensure the latest Account is loaded
304 setupOverviewMode();
305
Andy Huang9d3fd922012-09-26 22:23:58 -0700306 // Defer the call to initLoader with a Handler.
307 // We want to wait until we know which fragments are present and their final visibility
308 // states before going off and doing work. This prevents extraneous loading from occurring
309 // as the ViewPager shifts about before the initial position is set.
310 //
311 // e.g. click on item #10
312 // ViewPager.setAdapter() actually first loads #0 and #1 under the assumption that #0 is
313 // the initial primary item
314 // Then CPC immediately sets the primary item to #10, which tears down #0/#1 and sets up
315 // #9/#10/#11.
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700316 getHandler().post(new FragmentRunnable("showConversation", this) {
Andy Huang9d3fd922012-09-26 22:23:58 -0700317 @Override
318 public void go() {
319 showConversation();
320 }
321 });
Paul Westbrookcebcc642012-08-08 10:06:04 -0700322
Andrew Sapperstein606dbd72013-07-30 19:14:23 -0700323 if (mConversation != null && mConversation.conversationBaseUri != null &&
Andrew Sappersteinf59d01c2014-02-20 10:27:06 -0800324 !Utils.isEmpty(mAccount.accountCookieQueryUri)) {
Paul Westbrookcebcc642012-08-08 10:06:04 -0700325 // Set the cookie for this base url
Andrew Sappersteinf59d01c2014-02-20 10:27:06 -0800326 new SetCookieTask(getContext(), mConversation.conversationBaseUri.toString(),
327 mAccount.accountCookieQueryUri).execute();
Paul Westbrookcebcc642012-08-08 10:06:04 -0700328 }
Mindy Pereira9b875682012-02-15 18:10:54 -0800329 }
330
Mindy Pereira9b875682012-02-15 18:10:54 -0800331 @Override
Andy Huange964eee2012-10-02 19:24:58 -0700332 public void onCreate(Bundle savedState) {
333 super.onCreate(savedState);
334
Andrew Sappersteinb1d184d2013-08-09 14:14:31 -0700335 mWebViewClient = createConversationWebViewClient();
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700336
Andy Huange964eee2012-10-02 19:24:58 -0700337 if (savedState != null) {
338 mWebViewYPercent = savedState.getFloat(BUNDLE_KEY_WEBVIEW_Y_PERCENT);
339 }
Andrew Sapperstein3af481c2013-10-30 10:29:38 -0700340
Andrew Sapperstein2fd167d2014-01-28 10:07:38 -0800341 mBidiFormatter = BidiFormatter.getInstance();
Andy Huange964eee2012-10-02 19:24:58 -0700342 }
343
Andrew Sappersteinb1d184d2013-08-09 14:14:31 -0700344 protected ConversationWebViewClient createConversationWebViewClient() {
345 return new ConversationWebViewClient(mAccount);
346 }
347
Andy Huange964eee2012-10-02 19:24:58 -0700348 @Override
Mindy Pereira9b875682012-02-15 18:10:54 -0800349 public View onCreateView(LayoutInflater inflater,
350 ViewGroup container, Bundle savedInstanceState) {
Andy Huang839ada22012-07-20 15:48:40 -0700351
Andy Huang632721e2012-04-11 16:57:26 -0700352 View rootView = inflater.inflate(R.layout.conversation_view, container, false);
Andy Huangf70fc402012-02-17 15:37:42 -0800353 mConversationContainer = (ConversationContainer) rootView
354 .findViewById(R.id.conversation_container);
Andy Huang8f187782012-11-06 17:49:25 -0800355 mConversationContainer.setAccountController(this);
Andy Huang47aa9c92012-07-31 15:37:21 -0700356
Andrew Sapperstein85ea6182013-10-14 18:21:08 -0700357 final ViewGroup topmostOverlay =
358 (ViewGroup) mConversationContainer.findViewById(R.id.conversation_topmost_overlay);
359 inflateSnapHeader(topmostOverlay, inflater);
360 mConversationContainer.setupSnapHeader();
361
362 setupNewMessageBar();
Andy Huang47aa9c92012-07-31 15:37:21 -0700363
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700364 mProgressController = new ConversationViewProgressController(this, getHandler());
365 mProgressController.instantiateProgressIndicators(rootView);
mindyp3bcf1802012-09-09 11:17:00 -0700366
Andrew Sappersteine2a30e12014-07-02 22:36:56 -0700367 mWebView = (ConversationWebView)
368 mConversationContainer.findViewById(R.id.conversation_webview);
Andy Huangf70fc402012-02-17 15:37:42 -0800369
Andy Huangf70fc402012-02-17 15:37:42 -0800370 mWebView.addJavascriptInterface(mJsBridge, "mail");
mindyp3bcf1802012-09-09 11:17:00 -0700371 // On JB or newer, we use the 'webkitAnimationStart' DOM event to signal load complete
372 // Below JB, try to speed up initial render by having the webview do supplemental draws to
373 // custom a software canvas.
mindypb941fdb2012-09-11 08:28:23 -0700374 // TODO(mindyp):
375 //PAGE READINESS SIGNAL FOR JELLYBEAN AND NEWER
376 // Notify the app on 'webkitAnimationStart' of a simple dummy element with a simple no-op
377 // animation that immediately runs on page load. The app uses this as a signal that the
378 // content is loaded and ready to draw, since WebView delays firing this event until the
379 // layers are composited and everything is ready to draw.
380 // This signal does not seem to be reliable, so just use the old method for now.
Andy Huangf7ac83f2013-07-15 15:48:31 -0700381 final boolean isJBOrLater = Utils.isRunningJellybeanOrLater();
382 final boolean isUserVisible = isUserVisible();
383 mWebView.setUseSoftwareLayer(!isJBOrLater);
384 mEnableContentReadySignal = isJBOrLater && isUserVisible;
385 mWebView.onUserVisibilityChanged(isUserVisible);
Andy Huang17a9cde2012-03-09 18:03:16 -0800386 mWebView.setWebViewClient(mWebViewClient);
Andy Huangc1fb9a92013-02-11 13:09:12 -0800387 final WebChromeClient wcc = new WebChromeClient() {
Andy Huangf70fc402012-02-17 15:37:42 -0800388 @Override
389 public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
Andrew Sapperstein8ec43e82013-12-17 18:27:55 -0800390 if (consoleMessage.messageLevel() == ConsoleMessage.MessageLevel.ERROR) {
391 LogUtils.wtf(LOG_TAG, "JS: %s (%s:%d) f=%s", consoleMessage.message(),
392 consoleMessage.sourceId(), consoleMessage.lineNumber(),
393 ConversationViewFragment.this);
394 } else {
395 LogUtils.i(LOG_TAG, "JS: %s (%s:%d) f=%s", consoleMessage.message(),
396 consoleMessage.sourceId(), consoleMessage.lineNumber(),
397 ConversationViewFragment.this);
398 }
Andy Huangf70fc402012-02-17 15:37:42 -0800399 return true;
400 }
Andy Huangc1fb9a92013-02-11 13:09:12 -0800401 };
402 mWebView.setWebChromeClient(wcc);
Andy Huangf70fc402012-02-17 15:37:42 -0800403
Andy Huang3233bff2012-03-20 19:38:45 -0700404 final WebSettings settings = mWebView.getSettings();
Andy Huangf70fc402012-02-17 15:37:42 -0800405
Greg Bullockf50aafa2014-03-22 02:17:00 +0100406 final ScrollIndicatorsView scrollIndicators =
Greg Bullock82d9f632014-04-29 10:56:12 +0200407 (ScrollIndicatorsView) rootView.findViewById(R.id.scroll_indicators);
Greg Bullockf50aafa2014-03-22 02:17:00 +0100408 scrollIndicators.setSourceView(mWebView);
Mark Wei56d83852012-09-19 14:28:50 -0700409
Andy Huangf70fc402012-02-17 15:37:42 -0800410 settings.setJavaScriptEnabled(true);
Andy Huangf70fc402012-02-17 15:37:42 -0800411
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700412 ConversationViewUtils.setTextZoom(getResources(), settings);
Andy Huangc319b552012-04-25 19:53:50 -0700413
Andy Huang51067132012-03-12 20:08:19 -0700414 mViewsCreated = true;
Andy Huange964eee2012-10-02 19:24:58 -0700415 mWebViewLoadedData = false;
Andy Huang51067132012-03-12 20:08:19 -0700416
Mindy Pereira9b875682012-02-15 18:10:54 -0800417 return rootView;
418 }
419
Andrew Sapperstein85ea6182013-10-14 18:21:08 -0700420 protected void inflateSnapHeader(ViewGroup topmostOverlay, LayoutInflater inflater) {
421 inflater.inflate(R.layout.conversation_topmost_overlay_items, topmostOverlay, true);
422 }
423
424 protected void setupNewMessageBar() {
425 mNewMessageBar = (Button) mConversationContainer.findViewById(
426 R.id.new_message_notification_bar);
427 mNewMessageBar.setOnClickListener(new View.OnClickListener() {
428 @Override
429 public void onClick(View v) {
430 onNewMessageBarClick();
431 }
432 });
433 }
434
Mindy Pereira9b875682012-02-15 18:10:54 -0800435 @Override
Andy Huangf7ac83f2013-07-15 15:48:31 -0700436 public void onResume() {
437 super.onResume();
438 if (mWebView != null) {
439 mWebView.onResume();
440 }
441 }
442
443 @Override
444 public void onPause() {
445 super.onPause();
446 if (mWebView != null) {
447 mWebView.onPause();
448 }
449 }
450
451 @Override
Mindy Pereira9b875682012-02-15 18:10:54 -0800452 public void onDestroyView() {
Mindy Pereira9b875682012-02-15 18:10:54 -0800453 super.onDestroyView();
Andy Huang46dfba62012-04-19 01:47:32 -0700454 mConversationContainer.setOverlayAdapter(null);
455 mAdapter = null;
Andy Huang9d3fd922012-09-26 22:23:58 -0700456 resetLoadWaiting(); // be sure to unregister any active load observer
Andy Huang51067132012-03-12 20:08:19 -0700457 mViewsCreated = false;
Mindy Pereira9b875682012-02-15 18:10:54 -0800458 }
459
Andy Huange964eee2012-10-02 19:24:58 -0700460 @Override
461 public void onSaveInstanceState(Bundle outState) {
462 super.onSaveInstanceState(outState);
463
464 outState.putFloat(BUNDLE_KEY_WEBVIEW_Y_PERCENT, calculateScrollYPercent());
465 }
466
467 private float calculateScrollYPercent() {
Paul Westbrook1b56a672013-04-19 01:19:05 -0700468 final float p;
469 if (mWebView == null) {
470 // onCreateView hasn't been called, return 0 as the user hasn't scrolled the view.
471 return 0;
472 }
473
474 final int scrollY = mWebView.getScrollY();
475 final int viewH = mWebView.getHeight();
476 final int webH = (int) (mWebView.getContentHeight() * mWebView.getScale());
Andy Huange964eee2012-10-02 19:24:58 -0700477
478 if (webH == 0 || webH <= viewH) {
479 p = 0;
480 } else if (scrollY + viewH >= webH) {
481 // The very bottom is a special case, it acts as a stronger anchor than the scroll top
482 // at that point.
483 p = 1.0f;
484 } else {
485 p = (float) scrollY / webH;
486 }
487 return p;
488 }
489
Andy Huang9d3fd922012-09-26 22:23:58 -0700490 private void resetLoadWaiting() {
491 if (mLoadWaitReason == LOAD_WAIT_FOR_INITIAL_CONVERSATION) {
492 getListController().unregisterConversationLoadedObserver(mLoadedObserver);
493 }
494 mLoadWaitReason = LOAD_NOW;
495 }
496
Andy Huang5ff63742012-03-16 20:30:23 -0700497 @Override
mindypf4fce122012-09-14 15:55:33 -0700498 protected void markUnread() {
Vikram Aggarwald82a31f2013-02-05 15:03:00 -0800499 super.markUnread();
Andy Huang839ada22012-07-20 15:48:40 -0700500 // Ignore unsafe calls made after a fragment is detached from an activity
501 final ControllableActivity activity = (ControllableActivity) getActivity();
502 if (activity == null) {
503 LogUtils.w(LOG_TAG, "ignoring markUnread for conv=%s", mConversation.id);
504 return;
505 }
506
Andy Huang28e31e22012-07-26 16:33:15 -0700507 if (mViewState == null) {
508 LogUtils.i(LOG_TAG, "ignoring markUnread for conv with no view state (%d)",
509 mConversation.id);
510 return;
511 }
Andy Huang839ada22012-07-20 15:48:40 -0700512 activity.getConversationUpdater().markConversationMessagesUnread(mConversation,
Vikram Aggarwal4a878b62012-07-31 15:09:25 -0700513 mViewState.getUnreadMessageUris(), mViewState.getConversationInfo());
Andy Huang839ada22012-07-20 15:48:40 -0700514 }
515
mindypf4fce122012-09-14 15:55:33 -0700516 @Override
517 public void onUserVisibleHintChanged() {
Andy Huang9d3fd922012-09-26 22:23:58 -0700518 final boolean userVisible = isUserVisible();
Scott Kennedy58192e52013-05-08 16:35:57 -0700519 LogUtils.d(LOG_TAG, "ConversationViewFragment#onUserVisibleHintChanged(), userVisible = %b",
520 userVisible);
Andy Huang9d3fd922012-09-26 22:23:58 -0700521
522 if (!userVisible) {
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700523 mProgressController.dismissLoadingStatus();
Andy Huang9d3fd922012-09-26 22:23:58 -0700524 } else if (mViewsCreated) {
Andy Huange6c9fb62013-11-15 09:56:20 -0800525 String loadTag = null;
Andy Huangeeb4a352013-11-21 10:56:27 -0800526 final boolean isInitialLoading;
527 if (mActivity != null) {
528 isInitialLoading = mActivity.getConversationUpdater()
Andy Huange6c9fb62013-11-15 09:56:20 -0800529 .isInitialConversationLoading();
Andy Huangeeb4a352013-11-21 10:56:27 -0800530 } else {
531 isInitialLoading = true;
532 }
Andy Huange6c9fb62013-11-15 09:56:20 -0800533
Andy Huang9d3fd922012-09-26 22:23:58 -0700534 if (getMessageCursor() != null) {
535 LogUtils.d(LOG_TAG, "Fragment is now user-visible, onConversationSeen: %s", this);
Andy Huange6c9fb62013-11-15 09:56:20 -0800536 if (!isInitialLoading) {
537 loadTag = "preloaded";
538 }
Andy Huang9d3fd922012-09-26 22:23:58 -0700539 onConversationSeen();
540 } else if (isLoadWaiting()) {
541 LogUtils.d(LOG_TAG, "Fragment is now user-visible, showing conversation: %s", this);
Andy Huange6c9fb62013-11-15 09:56:20 -0800542 if (!isInitialLoading) {
543 loadTag = "load_deferred";
544 }
Andy Huang9d3fd922012-09-26 22:23:58 -0700545 handleDelayedConversationLoad();
546 }
Andy Huange6c9fb62013-11-15 09:56:20 -0800547
548 if (loadTag != null) {
549 // pager swipes are visibility transitions to 'visible' except during initial
550 // pager load on A) enter conversation mode B) rotate C) 2-pane conv-mode list-tap
551 Analytics.getInstance().sendEvent("pager_swipe", loadTag,
552 getCurrentFolderTypeDesc(), 0);
553 }
Andy Huang632721e2012-04-11 16:57:26 -0700554 }
Andy Huang632721e2012-04-11 16:57:26 -0700555
Andy Huang30bcfe72012-10-18 18:09:03 -0700556 if (mWebView != null) {
557 mWebView.onUserVisibilityChanged(userVisible);
558 }
Andy Huangf8cf5462012-10-17 18:29:14 -0700559 }
560
Andy Huang9d3fd922012-09-26 22:23:58 -0700561 /**
562 * Will either call initLoader now to begin loading, or set {@link #mLoadWaitReason} and do
563 * nothing (in which case you should later call {@link #handleDelayedConversationLoad()}).
564 */
Mindy Pereira9b875682012-02-15 18:10:54 -0800565 private void showConversation() {
Andy Huang9d3fd922012-09-26 22:23:58 -0700566 final int reason;
567
568 if (isUserVisible()) {
569 LogUtils.i(LOG_TAG,
570 "SHOWCONV: CVF is user-visible, immediately loading conversation (%s)", this);
571 reason = LOAD_NOW;
Andy Huang243c2362013-03-01 17:50:35 -0800572 timerMark("CVF.showConversation");
Andy Huang9d3fd922012-09-26 22:23:58 -0700573 } else {
574 final boolean disableOffscreenLoading = DISABLE_OFFSCREEN_LOADING
Alice Yang0b8c0802013-09-16 14:35:18 -0700575 || Utils.isLowRamDevice(getContext())
Andrew Sapperstein606dbd72013-07-30 19:14:23 -0700576 || (mConversation != null && (mConversation.isRemote
577 || mConversation.getNumMessages() > mMaxAutoLoadMessages));
Andy Huang9d3fd922012-09-26 22:23:58 -0700578
579 // When not visible, we should not immediately load if either this conversation is
580 // too heavyweight, or if the main/initial conversation is busy loading.
581 if (disableOffscreenLoading) {
582 reason = LOAD_WAIT_UNTIL_VISIBLE;
583 LogUtils.i(LOG_TAG, "SHOWCONV: CVF waiting until visible to load (%s)", this);
584 } else if (getListController().isInitialConversationLoading()) {
585 reason = LOAD_WAIT_FOR_INITIAL_CONVERSATION;
586 LogUtils.i(LOG_TAG, "SHOWCONV: CVF waiting for initial to finish (%s)", this);
587 getListController().registerConversationLoadedObserver(mLoadedObserver);
588 } else {
589 LogUtils.i(LOG_TAG,
590 "SHOWCONV: CVF is not visible, but no reason to wait. loading now. (%s)",
591 this);
592 reason = LOAD_NOW;
593 }
Andy Huang632721e2012-04-11 16:57:26 -0700594 }
Andy Huang9d3fd922012-09-26 22:23:58 -0700595
596 mLoadWaitReason = reason;
597 if (mLoadWaitReason == LOAD_NOW) {
598 startConversationLoad();
599 }
600 }
601
602 private void handleDelayedConversationLoad() {
603 resetLoadWaiting();
604 startConversationLoad();
605 }
606
607 private void startConversationLoad() {
mindyp3bcf1802012-09-09 11:17:00 -0700608 mWebView.setVisibility(View.VISIBLE);
Andrew Sapperstein606dbd72013-07-30 19:14:23 -0700609 loadContent();
mindyp3bcf1802012-09-09 11:17:00 -0700610 // TODO(mindyp): don't show loading status for a previously rendered
611 // conversation. Ielieve this is better done by making sure don't show loading status
612 // until XX ms have passed without loading completed.
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700613 mProgressController.showLoadingStatus(isUserVisible());
Mindy Pereira8e915722012-02-16 14:42:56 -0800614 }
615
Andrew Sapperstein606dbd72013-07-30 19:14:23 -0700616 /**
617 * Can be overridden in case a subclass needs to load something other than
618 * the messages of a conversation.
619 */
620 protected void loadContent() {
621 getLoaderManager().initLoader(MESSAGE_LOADER, Bundle.EMPTY, getMessageLoaderCallbacks());
622 }
623
Andy Huang7d4746e2012-10-17 17:03:17 -0700624 private void revealConversation() {
Andy Huang243c2362013-03-01 17:50:35 -0800625 timerMark("revealing conversation");
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700626 mProgressController.dismissLoadingStatus(mOnProgressDismiss);
Jin Cao72953f22014-04-15 18:23:37 -0700627 if (isUserVisible()) {
628 AnalyticsTimer.getInstance().logDuration(AnalyticsTimer.OPEN_CONV_VIEW_FROM_LIST,
629 true /* isDestructive */, "open_conversation", "from_list", null);
630 }
Andy Huang7d4746e2012-10-17 17:03:17 -0700631 }
632
Andy Huang9d3fd922012-09-26 22:23:58 -0700633 private boolean isLoadWaiting() {
634 return mLoadWaitReason != LOAD_NOW;
635 }
636
Andy Huang51067132012-03-12 20:08:19 -0700637 private void renderConversation(MessageCursor messageCursor) {
mindyp3bcf1802012-09-09 11:17:00 -0700638 final String convHtml = renderMessageBodies(messageCursor, mEnableContentReadySignal);
Andy Huang243c2362013-03-01 17:50:35 -0800639 timerMark("rendered conversation");
Andy Huangbd544e32012-05-29 15:56:51 -0700640
641 if (DEBUG_DUMP_CONVERSATION_HTML) {
642 java.io.FileWriter fw = null;
643 try {
Andrew Sappersteinf1566b12013-09-19 15:34:26 -0700644 fw = new java.io.FileWriter(getSdCardFilePath());
Andy Huangbd544e32012-05-29 15:56:51 -0700645 fw.write(convHtml);
646 } catch (java.io.IOException e) {
647 e.printStackTrace();
648 } finally {
649 if (fw != null) {
650 try {
651 fw.close();
652 } catch (java.io.IOException e) {
653 e.printStackTrace();
654 }
655 }
656 }
657 }
658
Andy Huange964eee2012-10-02 19:24:58 -0700659 // save off existing scroll position before re-rendering
660 if (mWebViewLoadedData) {
661 mWebViewYPercent = calculateScrollYPercent();
662 }
663
Andy Huangbd544e32012-05-29 15:56:51 -0700664 mWebView.loadDataWithBaseURL(mBaseUri, convHtml, "text/html", "utf-8", null);
Andy Huange964eee2012-10-02 19:24:58 -0700665 mWebViewLoadedData = true;
Andy Huang63b3c672012-10-05 19:27:28 -0700666 mWebViewLoadStartMs = SystemClock.uptimeMillis();
Andy Huang51067132012-03-12 20:08:19 -0700667 }
668
Andrew Sappersteinf1566b12013-09-19 15:34:26 -0700669 protected String getSdCardFilePath() {
670 return "/sdcard/conv" + mConversation.id + ".html";
671 }
672
Andy Huang7bdc3752012-03-25 17:18:19 -0700673 /**
674 * Populate the adapter with overlay views (message headers, super-collapsed blocks, a
675 * conversation header), and return an HTML document with spacer divs inserted for all overlays.
676 *
677 */
Andrew Sapperstein9f957f32013-07-19 15:18:18 -0700678 protected String renderMessageBodies(MessageCursor messageCursor,
mindyp3bcf1802012-09-09 11:17:00 -0700679 boolean enableContentReadySignal) {
Andy Huangf70fc402012-02-17 15:37:42 -0800680 int pos = -1;
Andy Huang632721e2012-04-11 16:57:26 -0700681
Andy Huang1ee96b22012-08-24 20:19:53 -0700682 LogUtils.d(LOG_TAG, "IN renderMessageBodies, fragment=%s", this);
Andy Huang7bdc3752012-03-25 17:18:19 -0700683 boolean allowNetworkImages = false;
684
Andy Huangc7543572012-04-03 15:34:29 -0700685 // TODO: re-use any existing adapter item state (expanded, details expanded, show pics)
Andy Huang28b7aee2012-08-20 20:27:32 -0700686
Andy Huang7bdc3752012-03-25 17:18:19 -0700687 // Walk through the cursor and build up an overlay adapter as you go.
688 // Each overlay has an entry in the adapter for easy scroll handling in the container.
689 // Items are not necessarily 1:1 in cursor and adapter because of super-collapsed blocks.
690 // When adding adapter items, also add their heights to help the container later determine
691 // overlay dimensions.
692
Andy Huangdb620fe2012-08-24 15:45:28 -0700693 // When re-rendering, prevent ConversationContainer from laying out overlays until after
694 // the new spacers are positioned by WebView.
695 mConversationContainer.invalidateSpacerGeometry();
696
Andy Huang7bdc3752012-03-25 17:18:19 -0700697 mAdapter.clear();
698
Andy Huang47aa9c92012-07-31 15:37:21 -0700699 // re-evaluate the message parts of the view state, since the messages may have changed
700 // since the previous render
701 final ConversationViewState prevState = mViewState;
702 mViewState = new ConversationViewState(prevState);
703
Andy Huang5ff63742012-03-16 20:30:23 -0700704 // N.B. the units of height for spacers are actually dp and not px because WebView assumes
Andy Huang2e9acfe2012-03-15 22:39:36 -0700705 // a pixel is an mdpi pixel, unless you set device-dpi.
Andy Huang5ff63742012-03-16 20:30:23 -0700706
Andy Huang7bdc3752012-03-25 17:18:19 -0700707 // add a single conversation header item
708 final int convHeaderPos = mAdapter.addConversationHeader(mConversation);
Andy Huang23014702012-07-09 12:50:36 -0700709 final int convHeaderPx = measureOverlayHeight(convHeaderPos);
Andy Huang5ff63742012-03-16 20:30:23 -0700710
Andy Huang4dc73232014-02-04 19:58:57 -0800711 mTemplates.startConversation(mWebView.getViewportWidth(),
712 mWebView.screenPxToWebPx(mSideMarginPx), mWebView.screenPxToWebPx(convHeaderPx));
Andy Huang3233bff2012-03-20 19:38:45 -0700713
Andy Huang46dfba62012-04-19 01:47:32 -0700714 int collapsedStart = -1;
Andy Huang839ada22012-07-20 15:48:40 -0700715 ConversationMessage prevCollapsedMsg = null;
Andrew Sappersteine8221482013-10-02 18:14:58 -0700716
Andrew Sapperstein8ec43e82013-12-17 18:27:55 -0800717 final boolean alwaysShowImages = shouldAlwaysShowImages();
Alice Yangf323c042013-10-30 00:15:02 -0700718
Andrew Sappersteine8221482013-10-02 18:14:58 -0700719 boolean prevSafeForImages = alwaysShowImages;
Andy Huang46dfba62012-04-19 01:47:32 -0700720
Andrew Sapperstein735a22a2014-07-11 03:57:42 -0700721 boolean hasDraft = false;
Andy Huangf70fc402012-02-17 15:37:42 -0800722 while (messageCursor.moveToPosition(++pos)) {
Andy Huang839ada22012-07-20 15:48:40 -0700723 final ConversationMessage msg = messageCursor.getMessage();
Andy Huang46dfba62012-04-19 01:47:32 -0700724
Andrew Sappersteine8221482013-10-02 18:14:58 -0700725 final boolean safeForImages = alwaysShowImages ||
Scott Kennedy20273842012-11-07 11:16:21 -0800726 msg.alwaysShowImages || prevState.getShouldShowImages(msg);
Andy Huang3233bff2012-03-20 19:38:45 -0700727 allowNetworkImages |= safeForImages;
Andy Huang24055282012-03-27 17:37:06 -0700728
Paul Westbrook08098ec2012-08-12 15:30:28 -0700729 final Integer savedExpanded = prevState.getExpansionState(msg);
730 final int expandedState;
Andy Huang839ada22012-07-20 15:48:40 -0700731 if (savedExpanded != null) {
Andy Huang1ee96b22012-08-24 20:19:53 -0700732 if (ExpansionState.isSuperCollapsed(savedExpanded) && messageCursor.isLast()) {
733 // override saved state when this is now the new last message
734 // this happens to the second-to-last message when you discard a draft
735 expandedState = ExpansionState.EXPANDED;
736 } else {
737 expandedState = savedExpanded;
738 }
Andy Huang839ada22012-07-20 15:48:40 -0700739 } else {
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700740 // new messages that are not expanded default to being eligible for super-collapse
Andrew Sapperstein605dcfc2014-07-03 14:43:35 -0700741 if (!msg.read || messageCursor.isLast()) {
742 expandedState = ExpansionState.EXPANDED;
743 } else if (messageCursor.isFirst()) {
744 expandedState = ExpansionState.COLLAPSED;
745 } else {
746 expandedState = ExpansionState.SUPER_COLLAPSED;
Andrew Sapperstein735a22a2014-07-11 03:57:42 -0700747 hasDraft |= msg.isDraft();
Andrew Sapperstein605dcfc2014-07-03 14:43:35 -0700748 }
Andy Huang839ada22012-07-20 15:48:40 -0700749 }
Scott Kennedy20273842012-11-07 11:16:21 -0800750 mViewState.setShouldShowImages(msg, prevState.getShouldShowImages(msg));
Paul Westbrook08098ec2012-08-12 15:30:28 -0700751 mViewState.setExpansionState(msg, expandedState);
Andy Huangc7543572012-04-03 15:34:29 -0700752
Andy Huang839ada22012-07-20 15:48:40 -0700753 // save off "read" state from the cursor
754 // later, the view may not match the cursor (e.g. conversation marked read on open)
Andy Huang423bea22012-08-21 12:00:49 -0700755 // however, if a previous state indicated this message was unread, trust that instead
756 // so "mark unread" marks all originally unread messages
757 mViewState.setReadState(msg, msg.read && !prevState.isUnread(msg));
Andy Huang839ada22012-07-20 15:48:40 -0700758
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700759 // We only want to consider this for inclusion in the super collapsed block if
760 // 1) The we don't have previous state about this message (The first time that the
761 // user opens a conversation)
762 // 2) The previously saved state for this message indicates that this message is
763 // in the super collapsed block.
764 if (ExpansionState.isSuperCollapsed(expandedState)) {
765 // contribute to a super-collapsed block that will be emitted just before the
766 // next expanded header
767 if (collapsedStart < 0) {
768 collapsedStart = pos;
Andy Huang46dfba62012-04-19 01:47:32 -0700769 }
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700770 prevCollapsedMsg = msg;
771 prevSafeForImages = safeForImages;
Andrew Sapperstein7dc7fa02013-05-08 16:39:31 -0700772
773 // This line puts the from address in the address cache so that
774 // we get the sender image for it if it's in a super-collapsed block.
775 getAddress(msg.getFrom());
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700776 continue;
Andy Huang46dfba62012-04-19 01:47:32 -0700777 }
Andy Huang24055282012-03-27 17:37:06 -0700778
Andy Huang46dfba62012-04-19 01:47:32 -0700779 // resolve any deferred decisions on previous collapsed items
780 if (collapsedStart >= 0) {
781 if (pos - collapsedStart == 1) {
Andrew Sappersteincee3c902013-07-31 10:52:02 -0700782 // Special-case for a single collapsed message: no need to super-collapse it.
Andrew Sappersteine2a30e12014-07-02 22:36:56 -0700783 renderMessage(prevCollapsedMsg, false /* expanded */, prevSafeForImages);
Andy Huang46dfba62012-04-19 01:47:32 -0700784 } else {
Andrew Sapperstein735a22a2014-07-11 03:57:42 -0700785 renderSuperCollapsedBlock(collapsedStart, pos - 1, hasDraft);
Andy Huang46dfba62012-04-19 01:47:32 -0700786 }
Andrew Sapperstein735a22a2014-07-11 03:57:42 -0700787 hasDraft = false; // reset hasDraft
Andy Huang46dfba62012-04-19 01:47:32 -0700788 prevCollapsedMsg = null;
789 collapsedStart = -1;
790 }
Andy Huang7bdc3752012-03-25 17:18:19 -0700791
Andrew Sappersteine2a30e12014-07-02 22:36:56 -0700792 renderMessage(msg, ExpansionState.isExpanded(expandedState), safeForImages);
Mindy Pereira9b875682012-02-15 18:10:54 -0800793 }
Andy Huang3233bff2012-03-20 19:38:45 -0700794
Andrew Sappersteine2a30e12014-07-02 22:36:56 -0700795 final MessageHeaderItem lastHeaderItem = getLastMessageHeaderItem();
796 final int convFooterPos = mAdapter.addConversationFooter(lastHeaderItem);
797 final int convFooterPx = measureOverlayHeight(convFooterPos);
798
Andy Huang3233bff2012-03-20 19:38:45 -0700799 mWebView.getSettings().setBlockNetworkImage(!allowNetworkImages);
800
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700801 final boolean applyTransforms = shouldApplyTransforms();
Andy Huang57f354c2013-04-11 17:23:40 -0700802
Andy Huangc1fb9a92013-02-11 13:09:12 -0800803 // If the conversation has specified a base uri, use it here, otherwise use mBaseUri
Andrew Sappersteine2a30e12014-07-02 22:36:56 -0700804 return mTemplates.endConversation(convFooterPx, mBaseUri,
805 mConversation.getBaseUri(mBaseUri),
Andy Huang2160d532014-02-10 15:43:14 -0800806 mWebView.getViewportWidth(), mWebView.getWidthInDp(mSideMarginPx),
807 enableContentReadySignal, isOverviewMode(mAccount), applyTransforms,
808 applyTransforms);
Mindy Pereira9b875682012-02-15 18:10:54 -0800809 }
Mindy Pereira674afa42012-02-17 14:05:24 -0800810
Andrew Sappersteine2a30e12014-07-02 22:36:56 -0700811 private MessageHeaderItem getLastMessageHeaderItem() {
812 final int count = mAdapter.getCount();
813 if (count < 3) {
814 LogUtils.wtf(LOG_TAG, "not enough items in the adapter. count: %s", count);
815 return null;
816 }
817 return (MessageHeaderItem) mAdapter.getItem(count - 2);
818 }
819
Andrew Sapperstein735a22a2014-07-11 03:57:42 -0700820 private void renderSuperCollapsedBlock(int start, int end, boolean hasDraft) {
821 final int blockPos = mAdapter.addSuperCollapsedBlock(start, end, hasDraft);
Andy Huang23014702012-07-09 12:50:36 -0700822 final int blockPx = measureOverlayHeight(blockPos);
823 mTemplates.appendSuperCollapsedHtml(start, mWebView.screenPxToWebPx(blockPx));
Andy Huang46dfba62012-04-19 01:47:32 -0700824 }
825
Andrew Sappersteine2a30e12014-07-02 22:36:56 -0700826 private void renderMessage(ConversationMessage msg, boolean expanded, boolean safeForImages) {
Andrew Sapperstein14f93742013-07-25 14:29:56 -0700827
Scott Kennedy20273842012-11-07 11:16:21 -0800828 final int headerPos = mAdapter.addMessageHeader(msg, expanded,
829 mViewState.getShouldShowImages(msg));
Andy Huang46dfba62012-04-19 01:47:32 -0700830 final MessageHeaderItem headerItem = (MessageHeaderItem) mAdapter.getItem(headerPos);
831
832 final int footerPos = mAdapter.addMessageFooter(headerItem);
833
834 // Measure item header and footer heights to allocate spacers in HTML
835 // But since the views themselves don't exist yet, render each item temporarily into
836 // a host view for measurement.
Andy Huang23014702012-07-09 12:50:36 -0700837 final int headerPx = measureOverlayHeight(headerPos);
838 final int footerPx = measureOverlayHeight(footerPos);
Andy Huang46dfba62012-04-19 01:47:32 -0700839
Andy Huang256b35c2012-08-22 15:19:13 -0700840 mTemplates.appendMessageHtml(msg, expanded, safeForImages,
Andy Huang23014702012-07-09 12:50:36 -0700841 mWebView.screenPxToWebPx(headerPx), mWebView.screenPxToWebPx(footerPx));
Andy Huang243c2362013-03-01 17:50:35 -0800842 timerMark("rendered message");
Andy Huang46dfba62012-04-19 01:47:32 -0700843 }
844
845 private String renderCollapsedHeaders(MessageCursor cursor,
846 SuperCollapsedBlockItem blockToReplace) {
847 final List<ConversationOverlayItem> replacements = Lists.newArrayList();
848
849 mTemplates.reset();
850
Alice Yangf323c042013-10-30 00:15:02 -0700851 final boolean alwaysShowImages = (mAccount != null) &&
852 (mAccount.settings.showImages == Settings.ShowImages.ALWAYS);
Andrew Sappersteine8221482013-10-02 18:14:58 -0700853
Mark Wei2b24e992012-09-10 16:40:07 -0700854 // In devices with non-integral density multiplier, screen pixels translate to non-integral
855 // web pixels. Keep track of the error that occurs when we cast all heights to int
856 float error = 0f;
Andrew Sapperstein14f93742013-07-25 14:29:56 -0700857 boolean first = true;
Andy Huang46dfba62012-04-19 01:47:32 -0700858 for (int i = blockToReplace.getStart(), end = blockToReplace.getEnd(); i <= end; i++) {
859 cursor.moveToPosition(i);
Andy Huang839ada22012-07-20 15:48:40 -0700860 final ConversationMessage msg = cursor.getMessage();
Andrew Sapperstein14f93742013-07-25 14:29:56 -0700861
Andrew Sapperstein4ddda2f2013-06-10 11:15:38 -0700862 final MessageHeaderItem header = ConversationViewAdapter.newMessageHeaderItem(
Andrew Sapperstein14f93742013-07-25 14:29:56 -0700863 mAdapter, mAdapter.getDateBuilder(), msg, false /* expanded */,
Andrew Sappersteine8221482013-10-02 18:14:58 -0700864 alwaysShowImages || mViewState.getShouldShowImages(msg));
Andrew Sapperstein381c3222014-04-20 12:23:57 -0700865 final MessageFooterItem footer = mAdapter.newMessageFooterItem(mAdapter, header);
Andy Huang46dfba62012-04-19 01:47:32 -0700866
Andy Huang23014702012-07-09 12:50:36 -0700867 final int headerPx = measureOverlayHeight(header);
868 final int footerPx = measureOverlayHeight(footer);
Mark Wei2b24e992012-09-10 16:40:07 -0700869 error += mWebView.screenPxToWebPxError(headerPx)
Andrew Sapperstein59ccec32014-06-18 17:22:49 -0700870 + mWebView.screenPxToWebPxError(footerPx);
Mark Wei2b24e992012-09-10 16:40:07 -0700871
872 // When the error becomes greater than 1 pixel, make the next header 1 pixel taller
873 int correction = 0;
874 if (error >= 1) {
875 correction = 1;
876 error -= 1;
877 }
Andy Huang46dfba62012-04-19 01:47:32 -0700878
Andrew Sappersteine8221482013-10-02 18:14:58 -0700879 mTemplates.appendMessageHtml(msg, false /* expanded */,
880 alwaysShowImages || msg.alwaysShowImages,
Mark Wei2b24e992012-09-10 16:40:07 -0700881 mWebView.screenPxToWebPx(headerPx) + correction,
882 mWebView.screenPxToWebPx(footerPx));
Andy Huang46dfba62012-04-19 01:47:32 -0700883 replacements.add(header);
884 replacements.add(footer);
Andy Huang839ada22012-07-20 15:48:40 -0700885
Paul Westbrook08098ec2012-08-12 15:30:28 -0700886 mViewState.setExpansionState(msg, ExpansionState.COLLAPSED);
Andy Huang46dfba62012-04-19 01:47:32 -0700887 }
888
889 mAdapter.replaceSuperCollapsedBlock(blockToReplace, replacements);
Andy Huang06c03622012-10-22 18:59:45 -0700890 mAdapter.notifyDataSetChanged();
Andy Huang46dfba62012-04-19 01:47:32 -0700891
892 return mTemplates.emit();
893 }
894
Andrew Sapperstein9f957f32013-07-19 15:18:18 -0700895 protected int measureOverlayHeight(int position) {
Andy Huang46dfba62012-04-19 01:47:32 -0700896 return measureOverlayHeight(mAdapter.getItem(position));
897 }
898
Andy Huang7bdc3752012-03-25 17:18:19 -0700899 /**
Andy Huangb8331b42012-07-16 19:08:53 -0700900 * Measure the height of an adapter view by rendering an adapter item into a temporary
Andy Huang46dfba62012-04-19 01:47:32 -0700901 * host view, and asking the view to immediately measure itself. This method will reuse
Andy Huang7bdc3752012-03-25 17:18:19 -0700902 * a previous adapter view from {@link ConversationContainer}'s scrap views if one was generated
903 * earlier.
904 * <p>
Andy Huang46dfba62012-04-19 01:47:32 -0700905 * After measuring the height, this method also saves the height in the
906 * {@link ConversationOverlayItem} for later use in overlay positioning.
Andy Huang7bdc3752012-03-25 17:18:19 -0700907 *
Andy Huang46dfba62012-04-19 01:47:32 -0700908 * @param convItem adapter item with data to render and measure
Andy Huang23014702012-07-09 12:50:36 -0700909 * @return height of the rendered view in screen px
Andy Huang7bdc3752012-03-25 17:18:19 -0700910 */
Andrew Sappersteinafaab172014-08-07 18:41:15 -0700911 private int measureOverlayHeight(ConversationOverlayItem convItem) {
Andy Huang7bdc3752012-03-25 17:18:19 -0700912 final int type = convItem.getType();
913
914 final View convertView = mConversationContainer.getScrapView(type);
Andy Huangb8331b42012-07-16 19:08:53 -0700915 final View hostView = mAdapter.getView(convItem, convertView, mConversationContainer,
916 true /* measureOnly */);
Andy Huang7bdc3752012-03-25 17:18:19 -0700917 if (convertView == null) {
918 mConversationContainer.addScrapView(type, hostView);
919 }
920
Andy Huang9875bb42012-04-04 20:36:21 -0700921 final int heightPx = mConversationContainer.measureOverlay(hostView);
Andy Huang7bdc3752012-03-25 17:18:19 -0700922 convItem.setHeight(heightPx);
Andy Huang9875bb42012-04-04 20:36:21 -0700923 convItem.markMeasurementValid();
Andy Huang7bdc3752012-03-25 17:18:19 -0700924
Andy Huang23014702012-07-09 12:50:36 -0700925 return heightPx;
Andy Huang7bdc3752012-03-25 17:18:19 -0700926 }
927
Andy Huang5ff63742012-03-16 20:30:23 -0700928 @Override
929 public void onConversationViewHeaderHeightChange(int newHeight) {
Mark Weiab2d9982012-09-25 13:06:17 -0700930 final int h = mWebView.screenPxToWebPx(newHeight);
931
932 mWebView.loadUrl(String.format("javascript:setConversationHeaderSpacerHeight(%s);", h));
Andy Huang5ff63742012-03-16 20:30:23 -0700933 }
934
Andy Huang3233bff2012-03-20 19:38:45 -0700935 // END conversation header callbacks
936
Andrew Sapperstein735a22a2014-07-11 03:57:42 -0700937 // START conversation footer callbacks
938
939 @Override
940 public void onConversationFooterHeightChange(int newHeight) {
941 final int h = mWebView.screenPxToWebPx(newHeight);
942
943 mWebView.loadUrl(String.format("javascript:setConversationFooterSpacerHeight(%s);", h));
944 }
945
946 // END conversation footer callbacks
947
Andy Huang3233bff2012-03-20 19:38:45 -0700948 // START message header callbacks
949 @Override
Andy Huangc7543572012-04-03 15:34:29 -0700950 public void setMessageSpacerHeight(MessageHeaderItem item, int newSpacerHeightPx) {
951 mConversationContainer.invalidateSpacerGeometry();
952
953 // update message HTML spacer height
Andy Huang23014702012-07-09 12:50:36 -0700954 final int h = mWebView.screenPxToWebPx(newSpacerHeightPx);
955 LogUtils.i(LAYOUT_TAG, "setting HTML spacer h=%dwebPx (%dscreenPx)", h,
956 newSpacerHeightPx);
Vikram Aggarwal5349ce12012-09-24 14:12:40 -0700957 mWebView.loadUrl(String.format("javascript:setMessageHeaderSpacerHeight('%s', %s);",
Andy Huang014ea4c2012-09-25 14:50:54 -0700958 mTemplates.getMessageDomId(item.getMessage()), h));
Andy Huang3233bff2012-03-20 19:38:45 -0700959 }
960
961 @Override
Andrew Sapperstein59ccec32014-06-18 17:22:49 -0700962 public void setMessageExpanded(MessageHeaderItem item, int newSpacerHeightPx) {
Andy Huangc7543572012-04-03 15:34:29 -0700963 mConversationContainer.invalidateSpacerGeometry();
964
965 // show/hide the HTML message body and update the spacer height
Andy Huang23014702012-07-09 12:50:36 -0700966 final int h = mWebView.screenPxToWebPx(newSpacerHeightPx);
967 LogUtils.i(LAYOUT_TAG, "setting HTML spacer expanded=%s h=%dwebPx (%dscreenPx)",
968 item.isExpanded(), h, newSpacerHeightPx);
Andrew Sapperstein59ccec32014-06-18 17:22:49 -0700969 mWebView.loadUrl(String.format("javascript:setMessageBodyVisible('%s', %s, %s);",
970 mTemplates.getMessageDomId(item.getMessage()), item.isExpanded(), h));
Andy Huang839ada22012-07-20 15:48:40 -0700971
Andy Huang014ea4c2012-09-25 14:50:54 -0700972 mViewState.setExpansionState(item.getMessage(),
Paul Westbrook08098ec2012-08-12 15:30:28 -0700973 item.isExpanded() ? ExpansionState.EXPANDED : ExpansionState.COLLAPSED);
Andy Huang3233bff2012-03-20 19:38:45 -0700974 }
975
976 @Override
Scott Kennedyeb9a4bd2012-11-12 10:33:04 -0800977 public void showExternalResources(final Message msg) {
Scott Kennedy20273842012-11-07 11:16:21 -0800978 mViewState.setShouldShowImages(msg, true);
Andy Huang3233bff2012-03-20 19:38:45 -0700979 mWebView.getSettings().setBlockNetworkImage(false);
Scott Kennedyeb9a4bd2012-11-12 10:33:04 -0800980 mWebView.loadUrl("javascript:unblockImages(['" + mTemplates.getMessageDomId(msg) + "']);");
981 }
982
983 @Override
984 public void showExternalResources(final String senderRawAddress) {
985 mWebView.getSettings().setBlockNetworkImage(false);
986
987 final Address sender = getAddress(senderRawAddress);
988 final MessageCursor cursor = getMessageCursor();
989
990 final List<String> messageDomIds = new ArrayList<String>();
991
992 int pos = -1;
993 while (cursor.moveToPosition(++pos)) {
994 final ConversationMessage message = cursor.getMessage();
995 if (sender.equals(getAddress(message.getFrom()))) {
996 message.alwaysShowImages = true;
997
998 mViewState.setShouldShowImages(message, true);
999 messageDomIds.add(mTemplates.getMessageDomId(message));
1000 }
1001 }
1002
1003 final String url = String.format(
1004 "javascript:unblockImages(['%s']);", TextUtils.join("','", messageDomIds));
1005 mWebView.loadUrl(url);
Andy Huang3233bff2012-03-20 19:38:45 -07001006 }
Alice Yang1ebc2db2013-03-14 21:21:44 -07001007
1008 @Override
Andy Huang75b52a52013-03-15 15:40:24 -07001009 public boolean supportsMessageTransforms() {
1010 return true;
1011 }
1012
1013 @Override
Alice Yang1ebc2db2013-03-14 21:21:44 -07001014 public String getMessageTransforms(final Message msg) {
1015 final String domId = mTemplates.getMessageDomId(msg);
1016 return (domId == null) ? null : mMessageTransforms.get(domId);
1017 }
1018
James Lemieux8e1ffbf2014-04-22 15:53:31 -07001019 @Override
1020 public boolean isSecure() {
1021 return false;
1022 }
1023
Andy Huang3233bff2012-03-20 19:38:45 -07001024 // END message header callbacks
Andy Huang5ff63742012-03-16 20:30:23 -07001025
Andy Huang46dfba62012-04-19 01:47:32 -07001026 @Override
Andrew Sapperstein2fc67302013-04-29 18:24:56 -07001027 public void showUntransformedConversation() {
1028 super.showUntransformedConversation();
1029 renderConversation(getMessageCursor());
1030 }
1031
1032 @Override
Andy Huang46dfba62012-04-19 01:47:32 -07001033 public void onSuperCollapsedClick(SuperCollapsedBlockItem item) {
mindypf4fce122012-09-14 15:55:33 -07001034 MessageCursor cursor = getMessageCursor();
1035 if (cursor == null || !mViewsCreated) {
Andy Huang46dfba62012-04-19 01:47:32 -07001036 return;
1037 }
1038
mindypf4fce122012-09-14 15:55:33 -07001039 mTempBodiesHtml = renderCollapsedHeaders(cursor, item);
Andy Huang46dfba62012-04-19 01:47:32 -07001040 mWebView.loadUrl("javascript:replaceSuperCollapsedBlock(" + item.getStart() + ")");
1041 }
1042
Andy Huang47aa9c92012-07-31 15:37:21 -07001043 private void showNewMessageNotification(NewMessagesInfo info) {
Andrew Sapperstein821fa872013-08-21 21:57:39 -07001044 mNewMessageBar.setText(info.getNotificationText());
Andy Huang47aa9c92012-07-31 15:37:21 -07001045 mNewMessageBar.setVisibility(View.VISIBLE);
1046 }
1047
1048 private void onNewMessageBarClick() {
1049 mNewMessageBar.setVisibility(View.GONE);
1050
mindypf4fce122012-09-14 15:55:33 -07001051 renderConversation(getMessageCursor()); // mCursor is already up-to-date
1052 // per onLoadFinished()
Andy Huang5fbda022012-02-28 18:22:03 -08001053 }
1054
Andrew Sappersteine2b2eb72014-05-13 10:25:34 -07001055 private static OverlayPosition[] parsePositions(final int[] topArray, final int[] bottomArray) {
Andy Huangadbf3e82012-10-13 13:30:19 -07001056 final int len = topArray.length;
1057 final OverlayPosition[] positions = new OverlayPosition[len];
Andy Huangb5078b22012-03-05 19:52:29 -08001058 for (int i = 0; i < len; i++) {
Andrew Sappersteine2b2eb72014-05-13 10:25:34 -07001059 positions[i] = new OverlayPosition(topArray[i], bottomArray[i]);
Andy Huangb5078b22012-03-05 19:52:29 -08001060 }
Andy Huangadbf3e82012-10-13 13:30:19 -07001061 return positions;
Andy Huangb5078b22012-03-05 19:52:29 -08001062 }
1063
Andrew Sapperstein9f957f32013-07-19 15:18:18 -07001064 protected Address getAddress(String rawFrom) {
Paul Westbrook0dfae692013-10-02 00:51:29 -07001065 return Utils.getAddress(mAddressCache, rawFrom);
Andy Huang16174812012-08-16 16:40:35 -07001066 }
1067
Andy Huang9d3fd922012-09-26 22:23:58 -07001068 private void ensureContentSizeChangeListener() {
1069 if (mWebViewSizeChangeListener == null) {
Andy Huangc1fb9a92013-02-11 13:09:12 -08001070 mWebViewSizeChangeListener = new ContentSizeChangeListener() {
Andy Huang9d3fd922012-09-26 22:23:58 -07001071 @Override
1072 public void onHeightChange(int h) {
1073 // When WebKit says the DOM height has changed, re-measure
1074 // bodies and re-position their headers.
1075 // This is separate from the typical JavaScript DOM change
1076 // listeners because cases like NARROW_COLUMNS text reflow do not trigger DOM
1077 // events.
1078 mWebView.loadUrl("javascript:measurePositions();");
1079 }
1080 };
1081 }
1082 mWebView.setContentSizeChangeListener(mWebViewSizeChangeListener);
1083 }
1084
Andrew Sapperstein9f957f32013-07-19 15:18:18 -07001085 public static boolean isOverviewMode(Account acct) {
Andy Huangccf67802013-03-15 14:31:57 -07001086 return acct.settings.isOverviewMode();
Andy Huangadbf3e82012-10-13 13:30:19 -07001087 }
1088
1089 private void setupOverviewMode() {
Andy Huang02f9d182012-11-28 22:38:02 -08001090 // for now, overview mode means use the built-in WebView zoom and disable custom scale
1091 // gesture handling
Andy Huangadbf3e82012-10-13 13:30:19 -07001092 final boolean overviewMode = isOverviewMode(mAccount);
1093 final WebSettings settings = mWebView.getSettings();
Andy Huang4dc73232014-02-04 19:58:57 -08001094 final WebSettings.LayoutAlgorithm layout;
Andy Huang06def562012-10-14 00:19:11 -07001095 settings.setUseWideViewPort(overviewMode);
Andy Huang57f354c2013-04-11 17:23:40 -07001096 settings.setSupportZoom(overviewMode);
1097 settings.setBuiltInZoomControls(overviewMode);
Andy Huang4dc73232014-02-04 19:58:57 -08001098 settings.setLoadWithOverviewMode(overviewMode);
Andy Huang57f354c2013-04-11 17:23:40 -07001099 if (overviewMode) {
1100 settings.setDisplayZoomControls(false);
Andy Huang4dc73232014-02-04 19:58:57 -08001101 layout = WebSettings.LayoutAlgorithm.NORMAL;
1102 } else {
1103 layout = WebSettings.LayoutAlgorithm.NARROW_COLUMNS;
Andy Huangadbf3e82012-10-13 13:30:19 -07001104 }
Andy Huang4dc73232014-02-04 19:58:57 -08001105 settings.setLayoutAlgorithm(layout);
Andy Huangadbf3e82012-10-13 13:30:19 -07001106 }
1107
Andy Huang3c6fd442014-03-24 19:56:46 -07001108 @Override
Andrew Sapperstein833123d2014-04-23 18:32:44 -07001109 public ConversationMessage getMessageForClickedUrl(String url) {
Andy Huang3c6fd442014-03-24 19:56:46 -07001110 final String domMessageId = mUrlToMessageIdMap.get(url);
1111 if (domMessageId == null) {
1112 return null;
1113 }
1114 final String messageId = mTemplates.getMessageIdForDomId(domMessageId);
1115 return getMessageCursor().getMessageForId(Long.parseLong(messageId));
1116 }
1117
Andrew Sappersteinb1d184d2013-08-09 14:14:31 -07001118 public class ConversationWebViewClient extends AbstractConversationWebViewClient {
Andrew Sapperstein4ddda2f2013-06-10 11:15:38 -07001119 public ConversationWebViewClient(Account account) {
1120 super(account);
Andrew Sapperstein376294b2013-06-06 16:04:26 -07001121 }
1122
Andy Huang17a9cde2012-03-09 18:03:16 -08001123 @Override
1124 public void onPageFinished(WebView view, String url) {
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001125 // Ignore unsafe calls made after a fragment is detached from an activity.
1126 // This method needs to, for example, get at the loader manager, which needs
1127 // the fragment to be added.
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001128 if (!isAdded() || !mViewsCreated) {
Paul Westbrook006e13c2013-07-24 18:40:20 -07001129 LogUtils.d(LOG_TAG, "ignoring CVF.onPageFinished, url=%s fragment=%s", url,
Andy Huangb95da852012-07-18 14:16:58 -07001130 ConversationViewFragment.this);
1131 return;
1132 }
1133
Paul Westbrook006e13c2013-07-24 18:40:20 -07001134 LogUtils.d(LOG_TAG, "IN CVF.onPageFinished, url=%s fragment=%s wv=%s t=%sms", url,
Andy Huang30bcfe72012-10-18 18:09:03 -07001135 ConversationViewFragment.this, view,
Andy Huang63b3c672012-10-05 19:27:28 -07001136 (SystemClock.uptimeMillis() - mWebViewLoadStartMs));
Andy Huang632721e2012-04-11 16:57:26 -07001137
Andy Huang9d3fd922012-09-26 22:23:58 -07001138 ensureContentSizeChangeListener();
1139
mindyp3bcf1802012-09-09 11:17:00 -07001140 if (!mEnableContentReadySignal) {
Andy Huang7d4746e2012-10-17 17:03:17 -07001141 revealConversation();
mindyp3bcf1802012-09-09 11:17:00 -07001142 }
Andy Huang9d3fd922012-09-26 22:23:58 -07001143
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001144 final Set<String> emailAddresses = Sets.newHashSet();
Andy Huang543e7092013-04-22 11:44:56 -07001145 final List<Address> cacheCopy;
1146 synchronized (mAddressCache) {
1147 cacheCopy = ImmutableList.copyOf(mAddressCache.values());
1148 }
1149 for (Address addr : cacheCopy) {
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001150 emailAddresses.add(addr.getAddress());
Andy Huangb8331b42012-07-16 19:08:53 -07001151 }
Andrew Sapperstein4ddda2f2013-06-10 11:15:38 -07001152 final ContactLoaderCallbacks callbacks = getContactInfoSource();
1153 callbacks.setSenders(emailAddresses);
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001154 getLoaderManager().restartLoader(CONTACT_LOADER, Bundle.EMPTY, callbacks);
Andy Huang17a9cde2012-03-09 18:03:16 -08001155 }
1156
Andy Huangaf5d4e02012-03-19 19:02:12 -07001157 @Override
1158 public boolean shouldOverrideUrlLoading(WebView view, String url) {
Paul Westbrook542fec92012-09-18 14:47:51 -07001159 return mViewsCreated && super.shouldOverrideUrlLoading(view, url);
Andy Huangaf5d4e02012-03-19 19:02:12 -07001160 }
Andy Huang17a9cde2012-03-09 18:03:16 -08001161 }
1162
Andy Huangf70fc402012-02-17 15:37:42 -08001163 /**
1164 * NOTE: all public methods must be listed in the proguard flags so that they can be accessed
1165 * via reflection and not stripped.
1166 *
1167 */
1168 private class MailJsBridge {
Mindy Pereira974c9662012-09-14 10:02:08 -07001169 @JavascriptInterface
Andrew Sappersteine2b2eb72014-05-13 10:25:34 -07001170 public void onWebContentGeometryChange(final int[] overlayTopStrs,
1171 final int[] overlayBottomStrs) {
Andrew Sapperstein8ec43e82013-12-17 18:27:55 -08001172 try {
1173 getHandler().post(new FragmentRunnable("onWebContentGeometryChange",
1174 ConversationViewFragment.this) {
1175 @Override
1176 public void go() {
Andy Huang46dfba62012-04-19 01:47:32 -07001177 if (!mViewsCreated) {
mindyp1b3cc472012-09-27 11:32:59 -07001178 LogUtils.d(LOG_TAG, "ignoring webContentGeometryChange because views"
1179 + " are gone, %s", ConversationViewFragment.this);
Andy Huang46dfba62012-04-19 01:47:32 -07001180 return;
1181 }
Andy Huangadbf3e82012-10-13 13:30:19 -07001182 mConversationContainer.onGeometryChange(
1183 parsePositions(overlayTopStrs, overlayBottomStrs));
mindyp1b3cc472012-09-27 11:32:59 -07001184 if (mDiff != 0) {
1185 // SCROLL!
1186 int scale = (int) (mWebView.getScale() / mWebView.getInitialScale());
1187 if (scale > 1) {
1188 mWebView.scrollBy(0, (mDiff * (scale - 1)));
1189 }
1190 mDiff = 0;
1191 }
Andy Huang46dfba62012-04-19 01:47:32 -07001192 }
Andrew Sapperstein8ec43e82013-12-17 18:27:55 -08001193 });
1194 } catch (Throwable t) {
1195 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onWebContentGeometryChange");
1196 }
Andy Huang46dfba62012-04-19 01:47:32 -07001197 }
1198
Mindy Pereira974c9662012-09-14 10:02:08 -07001199 @JavascriptInterface
Andy Huang46dfba62012-04-19 01:47:32 -07001200 public String getTempMessageBodies() {
1201 try {
1202 if (!mViewsCreated) {
1203 return "";
Andy Huangf70fc402012-02-17 15:37:42 -08001204 }
Andy Huang46dfba62012-04-19 01:47:32 -07001205
1206 final String s = mTempBodiesHtml;
1207 mTempBodiesHtml = null;
1208 return s;
1209 } catch (Throwable t) {
1210 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getTempMessageBodies");
1211 return "";
1212 }
Andy Huangf70fc402012-02-17 15:37:42 -08001213 }
1214
Andy Huang014ea4c2012-09-25 14:50:54 -07001215 @JavascriptInterface
1216 public String getMessageBody(String domId) {
1217 try {
1218 final MessageCursor cursor = getMessageCursor();
1219 if (!mViewsCreated || cursor == null) {
1220 return "";
1221 }
1222
1223 int pos = -1;
1224 while (cursor.moveToPosition(++pos)) {
1225 final ConversationMessage msg = cursor.getMessage();
1226 if (TextUtils.equals(domId, mTemplates.getMessageDomId(msg))) {
Andy Huang986776b2014-02-19 18:29:43 -08001227 return HtmlConversationTemplates.wrapMessageBody(msg.getBodyAsHtml());
Andy Huang014ea4c2012-09-25 14:50:54 -07001228 }
1229 }
1230
1231 return "";
1232
1233 } catch (Throwable t) {
1234 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getMessageBody");
1235 return "";
1236 }
1237 }
1238
Mindy Pereira974c9662012-09-14 10:02:08 -07001239 @JavascriptInterface
Andy Huang543e7092013-04-22 11:44:56 -07001240 public String getMessageSender(String domId) {
1241 try {
1242 final MessageCursor cursor = getMessageCursor();
1243 if (!mViewsCreated || cursor == null) {
1244 return "";
1245 }
1246
1247 int pos = -1;
1248 while (cursor.moveToPosition(++pos)) {
1249 final ConversationMessage msg = cursor.getMessage();
1250 if (TextUtils.equals(domId, mTemplates.getMessageDomId(msg))) {
1251 return getAddress(msg.getFrom()).getAddress();
1252 }
1253 }
1254
1255 return "";
1256
1257 } catch (Throwable t) {
1258 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getMessageSender");
1259 return "";
1260 }
1261 }
1262
Andy Huang543e7092013-04-22 11:44:56 -07001263 @JavascriptInterface
mindyp3bcf1802012-09-09 11:17:00 -07001264 public void onContentReady() {
Andrew Sapperstein8ec43e82013-12-17 18:27:55 -08001265 try {
1266 getHandler().post(new FragmentRunnable("onContentReady",
1267 ConversationViewFragment.this) {
1268 @Override
1269 public void go() {
1270 try {
1271 if (mWebViewLoadStartMs != 0) {
1272 LogUtils.i(LOG_TAG, "IN CVF.onContentReady, f=%s vis=%s t=%sms",
1273 ConversationViewFragment.this,
1274 isUserVisible(),
1275 (SystemClock.uptimeMillis() - mWebViewLoadStartMs));
1276 }
1277 revealConversation();
1278 } catch (Throwable t) {
1279 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onContentReady");
1280 // Still try to show the conversation.
1281 revealConversation();
Andy Huang63b3c672012-10-05 19:27:28 -07001282 }
mindyp3bcf1802012-09-09 11:17:00 -07001283 }
Andrew Sapperstein8ec43e82013-12-17 18:27:55 -08001284 });
1285 } catch (Throwable t) {
1286 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onContentReady");
1287 }
mindyp3bcf1802012-09-09 11:17:00 -07001288 }
Andy Huange964eee2012-10-02 19:24:58 -07001289
Andy Huange964eee2012-10-02 19:24:58 -07001290 @JavascriptInterface
1291 public float getScrollYPercent() {
1292 try {
1293 return mWebViewYPercent;
1294 } catch (Throwable t) {
1295 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getScrollYPercent");
1296 return 0f;
1297 }
1298 }
Andy Huang05c70c82013-03-14 15:15:50 -07001299
Andy Huang05c70c82013-03-14 15:15:50 -07001300 @JavascriptInterface
1301 public void onMessageTransform(String messageDomId, String transformText) {
Andrew Sappersteinae92e152013-05-03 13:55:18 -07001302 try {
1303 LogUtils.i(LOG_TAG, "TRANSFORM: (%s) %s", messageDomId, transformText);
1304 mMessageTransforms.put(messageDomId, transformText);
1305 onConversationTransformed();
1306 } catch (Throwable t) {
1307 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onMessageTransform");
Andrew Sapperstein8ec43e82013-12-17 18:27:55 -08001308 }
1309 }
1310
1311 @JavascriptInterface
1312 public void onInlineAttachmentsParsed(final String[] urls, final String[] messageIds) {
1313 try {
1314 getHandler().post(new FragmentRunnable("onInlineAttachmentsParsed",
1315 ConversationViewFragment.this) {
1316 @Override
1317 public void go() {
1318 try {
1319 for (int i = 0, size = urls.length; i < size; i++) {
1320 mUrlToMessageIdMap.put(urls[i], messageIds[i]);
1321 }
1322 } catch (ArrayIndexOutOfBoundsException e) {
1323 LogUtils.e(LOG_TAG, e,
1324 "Number of urls does not match number of message ids - %s:%s",
1325 urls.length, messageIds.length);
1326 }
1327 }
1328 });
1329 } catch (Throwable t) {
1330 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onInlineAttachmentsParsed");
Andrew Sappersteinae92e152013-05-03 13:55:18 -07001331 }
Andy Huang05c70c82013-03-14 15:15:50 -07001332 }
Andy Huangf70fc402012-02-17 15:37:42 -08001333 }
1334
Andy Huang47aa9c92012-07-31 15:37:21 -07001335 private class NewMessagesInfo {
1336 int count;
Andy Huang06c03622012-10-22 18:59:45 -07001337 int countFromSelf;
Andy Huang47aa9c92012-07-31 15:37:21 -07001338 String senderAddress;
1339
1340 /**
1341 * Return the display text for the new message notification overlay. It will be formatted
1342 * appropriately for a single new message vs. multiple new messages.
1343 *
1344 * @return display text
1345 */
1346 public String getNotificationText() {
mindypad0c30d2012-09-25 12:09:13 -07001347 Resources res = getResources();
Andy Huang47aa9c92012-07-31 15:37:21 -07001348 if (count > 1) {
Andrew Sapperstein66d69112013-08-23 12:24:44 -07001349 return res.getQuantityString(R.plurals.new_incoming_messages_many, count, count);
Andy Huang47aa9c92012-07-31 15:37:21 -07001350 } else {
Andy Huang16174812012-08-16 16:40:35 -07001351 final Address addr = getAddress(senderAddress);
mindypad0c30d2012-09-25 12:09:13 -07001352 return res.getString(R.string.new_incoming_messages_one,
Andrew Sapperstein2fd167d2014-01-28 10:07:38 -08001353 mBidiFormatter.unicodeWrap(TextUtils.isEmpty(addr.getPersonal())
1354 ? addr.getAddress() : addr.getPersonal()));
Andy Huang47aa9c92012-07-31 15:37:21 -07001355 }
Andy Huang47aa9c92012-07-31 15:37:21 -07001356 }
1357 }
1358
mindypf4fce122012-09-14 15:55:33 -07001359 @Override
Paul Westbrookc42ad5e2013-05-09 16:52:15 -07001360 public void onMessageCursorLoadFinished(Loader<ObjectCursor<ConversationMessage>> loader,
1361 MessageCursor newCursor, MessageCursor oldCursor) {
mindypf4fce122012-09-14 15:55:33 -07001362 /*
1363 * what kind of changes affect the MessageCursor? 1. new message(s) 2.
1364 * read/unread state change 3. deleted message, either regular or draft
1365 * 4. updated message, either from self or from others, updated in
1366 * content or state or sender 5. star/unstar of message (technically
1367 * similar to #1) 6. other label change Use MessageCursor.hashCode() to
1368 * sort out interesting vs. no-op cursor updates.
1369 */
Andy Huangb8331b42012-07-16 19:08:53 -07001370
Andy Huang233d4352012-10-18 14:00:24 -07001371 if (oldCursor != null && !oldCursor.isClosed()) {
Andy Huang014ea4c2012-09-25 14:50:54 -07001372 final NewMessagesInfo info = getNewIncomingMessagesInfo(newCursor);
Andy Huangb8331b42012-07-16 19:08:53 -07001373
Andy Huang014ea4c2012-09-25 14:50:54 -07001374 if (info.count > 0) {
1375 // don't immediately render new incoming messages from other
1376 // senders
1377 // (to avoid a new message from losing the user's focus)
1378 LogUtils.i(LOG_TAG, "CONV RENDER: conversation updated"
Andy Huang9d3fd922012-09-26 22:23:58 -07001379 + ", holding cursor for new incoming message (%s)", this);
Andy Huang014ea4c2012-09-25 14:50:54 -07001380 showNewMessageNotification(info);
1381 return;
1382 }
1383
Andy Huang06c03622012-10-22 18:59:45 -07001384 final int oldState = oldCursor.getStateHashCode();
1385 final boolean changed = newCursor.getStateHashCode() != oldState;
Andy Huang233d4352012-10-18 14:00:24 -07001386
Andy Huang014ea4c2012-09-25 14:50:54 -07001387 if (!changed) {
1388 final boolean processedInPlace = processInPlaceUpdates(newCursor, oldCursor);
1389 if (processedInPlace) {
Andy Huang9d3fd922012-09-26 22:23:58 -07001390 LogUtils.i(LOG_TAG, "CONV RENDER: processed update(s) in place (%s)", this);
Andy Huang1ee96b22012-08-24 20:19:53 -07001391 } else {
mindypf4fce122012-09-14 15:55:33 -07001392 LogUtils.i(LOG_TAG, "CONV RENDER: uninteresting update"
Andy Huang9d3fd922012-09-26 22:23:58 -07001393 + ", ignoring this conversation update (%s)", this);
Andy Huang1ee96b22012-08-24 20:19:53 -07001394 }
Andy Huangb8331b42012-07-16 19:08:53 -07001395 return;
Andy Huang06c03622012-10-22 18:59:45 -07001396 } else if (info.countFromSelf == 1) {
1397 // Special-case the very common case of a new cursor that is the same as the old
1398 // one, except that there is a new message from yourself. This happens upon send.
1399 final boolean sameExceptNewLast = newCursor.getStateHashCode(1) == oldState;
1400 if (sameExceptNewLast) {
1401 LogUtils.i(LOG_TAG, "CONV RENDER: update is a single new message from self"
1402 + " (%s)", this);
1403 newCursor.moveToLast();
1404 processNewOutgoingMessage(newCursor.getMessage());
1405 return;
1406 }
Andy Huangb8331b42012-07-16 19:08:53 -07001407 }
Andy Huang6766b6e2012-09-28 12:43:52 -07001408 // cursors are different, and not due to an incoming message. fall
1409 // through and render.
1410 LogUtils.i(LOG_TAG, "CONV RENDER: conversation updated"
1411 + ", but not due to incoming message. rendering. (%s)", this);
Andy Huang06c03622012-10-22 18:59:45 -07001412
1413 if (DEBUG_DUMP_CURSOR_CONTENTS) {
1414 LogUtils.i(LOG_TAG, "old cursor: %s", oldCursor.getDebugDump());
1415 LogUtils.i(LOG_TAG, "new cursor: %s", newCursor.getDebugDump());
1416 }
Andy Huang6766b6e2012-09-28 12:43:52 -07001417 } else {
1418 LogUtils.i(LOG_TAG, "CONV RENDER: initial render. (%s)", this);
Andy Huang243c2362013-03-01 17:50:35 -08001419 timerMark("message cursor load finished");
Andy Huangb8331b42012-07-16 19:08:53 -07001420 }
1421
Andrew Sapperstein606dbd72013-07-30 19:14:23 -07001422 renderContent(newCursor);
1423 }
1424
1425 protected void renderContent(MessageCursor messageCursor) {
Mark Wei4071c2f2012-09-26 14:38:38 -07001426 // if layout hasn't happened, delay render
1427 // This is needed in addition to the showConversation() delay to speed
1428 // up rotation and restoration.
1429 if (mConversationContainer.getWidth() == 0) {
1430 mNeedRender = true;
1431 mConversationContainer.addOnLayoutChangeListener(this);
1432 } else {
Andrew Sapperstein606dbd72013-07-30 19:14:23 -07001433 renderConversation(messageCursor);
Mark Wei4071c2f2012-09-26 14:38:38 -07001434 }
Andy Huangb8331b42012-07-16 19:08:53 -07001435 }
1436
mindypf4fce122012-09-14 15:55:33 -07001437 private NewMessagesInfo getNewIncomingMessagesInfo(MessageCursor newCursor) {
1438 final NewMessagesInfo info = new NewMessagesInfo();
Andy Huangb8331b42012-07-16 19:08:53 -07001439
mindypf4fce122012-09-14 15:55:33 -07001440 int pos = -1;
1441 while (newCursor.moveToPosition(++pos)) {
1442 final Message m = newCursor.getMessage();
1443 if (!mViewState.contains(m)) {
1444 LogUtils.i(LOG_TAG, "conversation diff: found new msg: %s", m.uri);
Andy Huangb8331b42012-07-16 19:08:53 -07001445
Scott Kennedy8960f0a2012-11-07 15:35:50 -08001446 final Address from = getAddress(m.getFrom());
mindypf4fce122012-09-14 15:55:33 -07001447 // distinguish ours from theirs
1448 // new messages from the account owner should not trigger a
1449 // notification
1450 if (mAccount.ownsFromAddress(from.getAddress())) {
1451 LogUtils.i(LOG_TAG, "found message from self: %s", m.uri);
Andy Huang06c03622012-10-22 18:59:45 -07001452 info.countFromSelf++;
mindypf4fce122012-09-14 15:55:33 -07001453 continue;
1454 }
Andy Huangb8331b42012-07-16 19:08:53 -07001455
mindypf4fce122012-09-14 15:55:33 -07001456 info.count++;
Scott Kennedy8960f0a2012-11-07 15:35:50 -08001457 info.senderAddress = m.getFrom();
Andy Huangb8331b42012-07-16 19:08:53 -07001458 }
Andy Huangb8331b42012-07-16 19:08:53 -07001459 }
mindypf4fce122012-09-14 15:55:33 -07001460 return info;
Andy Huangb8331b42012-07-16 19:08:53 -07001461 }
1462
Andy Huang014ea4c2012-09-25 14:50:54 -07001463 private boolean processInPlaceUpdates(MessageCursor newCursor, MessageCursor oldCursor) {
1464 final Set<String> idsOfChangedBodies = Sets.newHashSet();
Andy Huang6b3d0d92012-10-30 15:46:48 -07001465 final List<Integer> changedOverlayPositions = Lists.newArrayList();
1466
Andy Huang014ea4c2012-09-25 14:50:54 -07001467 boolean changed = false;
1468
1469 int pos = 0;
1470 while (true) {
1471 if (!newCursor.moveToPosition(pos) || !oldCursor.moveToPosition(pos)) {
1472 break;
1473 }
1474
1475 final ConversationMessage newMsg = newCursor.getMessage();
1476 final ConversationMessage oldMsg = oldCursor.getMessage();
1477
Jin Cao2ef5f082014-03-31 14:52:38 -07001478 // We are going to update the data in the adapter whenever any input fields change.
1479 // This ensures that the Message object that ComposeActivity uses will be correctly
1480 // aligned with the most up-to-date data.
1481 if (!newMsg.isEqual(oldMsg)) {
Andy Huang6b3d0d92012-10-30 15:46:48 -07001482 mAdapter.updateItemsForMessage(newMsg, changedOverlayPositions);
Jin Cao6a2df252014-05-29 15:12:33 -07001483 LogUtils.i(LOG_TAG, "msg #%d (%d): detected field(s) change. sendingState=%s",
1484 pos, newMsg.id, newMsg.sendingState);
Andy Huang014ea4c2012-09-25 14:50:54 -07001485 }
1486
1487 // update changed message bodies in-place
1488 if (!TextUtils.equals(newMsg.bodyHtml, oldMsg.bodyHtml) ||
1489 !TextUtils.equals(newMsg.bodyText, oldMsg.bodyText)) {
1490 // maybe just set a flag to notify JS to re-request changed bodies
1491 idsOfChangedBodies.add('"' + mTemplates.getMessageDomId(newMsg) + '"');
1492 LogUtils.i(LOG_TAG, "msg #%d (%d): detected body change", pos, newMsg.id);
1493 }
1494
1495 pos++;
1496 }
1497
Andy Huang6b3d0d92012-10-30 15:46:48 -07001498
1499 if (!changedOverlayPositions.isEmpty()) {
Andy Huang06c03622012-10-22 18:59:45 -07001500 // notify once after the entire adapter is updated
Andy Huang6b3d0d92012-10-30 15:46:48 -07001501 mConversationContainer.onOverlayModelUpdate(changedOverlayPositions);
1502 changed = true;
Andy Huang06c03622012-10-22 18:59:45 -07001503 }
1504
Andrew Sapperstein735a22a2014-07-11 03:57:42 -07001505 final ConversationFooterItem footerItem = mAdapter.getFooterItem();
1506 if (footerItem != null) {
1507 footerItem.invalidateMeasurement();
1508 }
Andy Huang014ea4c2012-09-25 14:50:54 -07001509 if (!idsOfChangedBodies.isEmpty()) {
1510 mWebView.loadUrl(String.format("javascript:replaceMessageBodies([%s]);",
1511 TextUtils.join(",", idsOfChangedBodies)));
1512 changed = true;
1513 }
1514
1515 return changed;
1516 }
1517
Andy Huang06c03622012-10-22 18:59:45 -07001518 private void processNewOutgoingMessage(ConversationMessage msg) {
Andrew Sappersteine2a30e12014-07-02 22:36:56 -07001519 // Temporarily remove the ConversationFooterItem and its view.
1520 // It will get re-added right after the new message is added.
1521 final ConversationFooterItem footerItem = mAdapter.removeFooterItem();
1522 mConversationContainer.removeViewAtAdapterIndex(footerItem.getPosition());
Andy Huang06c03622012-10-22 18:59:45 -07001523 mTemplates.reset();
1524 // this method will add some items to mAdapter, but we deliberately want to avoid notifying
1525 // adapter listeners (i.e. ConversationContainer) until onWebContentGeometryChange is next
1526 // called, to prevent N+1 headers rendering with N message bodies.
Andrew Sappersteine2a30e12014-07-02 22:36:56 -07001527 renderMessage(msg, true /* expanded */, msg.alwaysShowImages);
Andy Huang06c03622012-10-22 18:59:45 -07001528 mTempBodiesHtml = mTemplates.emit();
1529
Andrew Sappersteine2a30e12014-07-02 22:36:56 -07001530 if (footerItem != null) {
1531 footerItem.setLastMessageHeaderItem(getLastMessageHeaderItem());
Andrew Sapperstein735a22a2014-07-11 03:57:42 -07001532 footerItem.invalidateMeasurement();
Andrew Sappersteine2a30e12014-07-02 22:36:56 -07001533 mAdapter.addItem(footerItem);
1534 }
1535
Andy Huang06c03622012-10-22 18:59:45 -07001536 mViewState.setExpansionState(msg, ExpansionState.EXPANDED);
1537 // FIXME: should the provider set this as initial state?
1538 mViewState.setReadState(msg, false /* read */);
1539
Andy Huang91d782a2012-10-25 12:37:29 -07001540 // From now until the updated spacer geometry is returned, the adapter items are mismatched
1541 // with the existing spacers. Do not let them layout.
1542 mConversationContainer.invalidateSpacerGeometry();
1543
Andy Huang06c03622012-10-22 18:59:45 -07001544 mWebView.loadUrl("javascript:appendMessageHtml();");
1545 }
1546
Andrew Sappersteinf59d01c2014-02-20 10:27:06 -08001547 private static class SetCookieTask extends AsyncTask<Void, Void, Void> {
1548 private final Context mContext;
1549 private final String mUri;
1550 private final Uri mAccountCookieQueryUri;
1551 private final ContentResolver mResolver;
Paul Westbrookcebcc642012-08-08 10:06:04 -07001552
Andrew Sappersteinf59d01c2014-02-20 10:27:06 -08001553 /* package */ SetCookieTask(Context context, String baseUri, Uri accountCookieQueryUri) {
1554 mContext = context;
1555 mUri = baseUri;
Paul Westbrookb8361c92012-09-27 10:57:14 -07001556 mAccountCookieQueryUri = accountCookieQueryUri;
1557 mResolver = context.getContentResolver();
Paul Westbrookcebcc642012-08-08 10:06:04 -07001558 }
1559
1560 @Override
1561 public Void doInBackground(Void... args) {
Andrew Sappersteinf59d01c2014-02-20 10:27:06 -08001562 // First query for the cookie string from the UI provider
Paul Westbrookb8361c92012-09-27 10:57:14 -07001563 final Cursor cookieCursor = mResolver.query(mAccountCookieQueryUri,
1564 UIProvider.ACCOUNT_COOKIE_PROJECTION, null, null, null);
1565 if (cookieCursor == null) {
1566 return null;
1567 }
1568
1569 try {
1570 if (cookieCursor.moveToFirst()) {
1571 final String cookie = cookieCursor.getString(
1572 cookieCursor.getColumnIndex(UIProvider.AccountCookieColumns.COOKIE));
1573
1574 if (cookie != null) {
1575 final CookieSyncManager csm =
Andrew Sappersteinf59d01c2014-02-20 10:27:06 -08001576 CookieSyncManager.createInstance(mContext);
Paul Westbrookb8361c92012-09-27 10:57:14 -07001577 CookieManager.getInstance().setCookie(mUri, cookie);
1578 csm.sync();
1579 }
1580 }
1581
1582 } finally {
1583 cookieCursor.close();
1584 }
1585
1586
Paul Westbrookcebcc642012-08-08 10:06:04 -07001587 return null;
1588 }
1589 }
mindyp36280f32012-09-09 16:11:23 -07001590
mindyp26d4d2d2012-09-18 17:30:32 -07001591 @Override
mindyp36280f32012-09-09 16:11:23 -07001592 public void onConversationUpdated(Conversation conv) {
1593 final ConversationViewHeader headerView = (ConversationViewHeader) mConversationContainer
1594 .findViewById(R.id.conversation_header);
mindypb2b98ba2012-09-24 14:13:58 -07001595 mConversation = conv;
mindyp9e0b2362012-09-09 16:31:21 -07001596 if (headerView != null) {
1597 headerView.onConversationUpdated(conv);
1598 }
mindyp36280f32012-09-09 16:11:23 -07001599 }
Mark Wei4071c2f2012-09-26 14:38:38 -07001600
1601 @Override
1602 public void onLayoutChange(View v, int left, int top, int right,
1603 int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
1604 boolean sizeChanged = mNeedRender
1605 && mConversationContainer.getWidth() != 0;
1606 if (sizeChanged) {
1607 mNeedRender = false;
1608 mConversationContainer.removeOnLayoutChangeListener(this);
1609 renderConversation(getMessageCursor());
1610 }
1611 }
mindyp1b3cc472012-09-27 11:32:59 -07001612
1613 @Override
James Lemieux7cad2802014-01-09 15:00:53 -08001614 public void setMessageDetailsExpanded(MessageHeaderItem i, boolean expanded, int heightBefore) {
mindyp1b3cc472012-09-27 11:32:59 -07001615 mDiff = (expanded ? 1 : -1) * Math.abs(i.getHeight() - heightBefore);
1616 }
Andy Huang02f9d182012-11-28 22:38:02 -08001617
James Lemieux7cad2802014-01-09 15:00:53 -08001618 /**
1619 * @return {@code true} because either the Print or Print All menu item is shown in GMail
1620 */
1621 @Override
1622 protected boolean shouldShowPrintInOverflow() {
1623 return true;
1624 }
1625
1626 @Override
Andrew Sapperstein5c1692a2013-09-16 11:56:13 -07001627 protected void printConversation() {
Andrew Sapperstein234d3532013-10-29 14:54:04 -07001628 PrintUtils.printConversation(mActivity.getActivityContext(), getMessageCursor(),
1629 mAddressCache, mConversation.getBaseUri(mBaseUri), true /* useJavascript */);
Andrew Sapperstein5c1692a2013-09-16 11:56:13 -07001630 }
Mindy Pereira9b875682012-02-15 18:10:54 -08001631}