blob: c53264b07c22fb38eb6a3aad142d2e25a473b488 [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;
Jin Cao0b693382014-08-11 10:46:12 -070026import android.graphics.Rect;
Paul Westbrookb8361c92012-09-27 10:57:14 -070027import android.net.Uri;
Paul Westbrookcebcc642012-08-08 10:06:04 -070028import android.os.AsyncTask;
Mindy Pereira9b875682012-02-15 18:10:54 -080029import android.os.Bundle;
mindyp3bcf1802012-09-09 11:17:00 -070030import android.os.SystemClock;
Jin Caoc966a8a2014-08-21 17:29:53 -070031import android.support.annotation.IdRes;
Tony Mantlerfa674292014-09-11 13:33:56 -070032import android.support.annotation.Nullable;
Andrew Sapperstein3af481c2013-10-30 10:29:38 -070033import android.support.v4.text.BidiFormatter;
Andrew Sapperstein8ec43e82013-12-17 18:27:55 -080034import android.support.v4.util.ArrayMap;
Andy Huang47aa9c92012-07-31 15:37:21 -070035import android.text.TextUtils;
Jin Caoa7404582014-08-05 14:03:59 -070036import android.view.KeyEvent;
Mindy Pereira9b875682012-02-15 18:10:54 -080037import android.view.LayoutInflater;
Andy Huangc1fb9a92013-02-11 13:09:12 -080038import android.view.View;
Mark Wei4071c2f2012-09-26 14:38:38 -070039import android.view.View.OnLayoutChangeListener;
Mindy Pereira9b875682012-02-15 18:10:54 -080040import android.view.ViewGroup;
Andy Huangf70fc402012-02-17 15:37:42 -080041import android.webkit.ConsoleMessage;
Paul Westbrookcebcc642012-08-08 10:06:04 -070042import android.webkit.CookieManager;
43import android.webkit.CookieSyncManager;
Mindy Pereira974c9662012-09-14 10:02:08 -070044import android.webkit.JavascriptInterface;
Andy Huangf70fc402012-02-17 15:37:42 -080045import android.webkit.WebChromeClient;
James Lemieuxe1302c32014-08-21 15:57:46 -070046import android.webkit.WebResourceResponse;
Andy Huangf70fc402012-02-17 15:37:42 -080047import android.webkit.WebSettings;
Andy Huang17a9cde2012-03-09 18:03:16 -080048import android.webkit.WebView;
Mindy Pereira9b875682012-02-15 18:10:54 -080049
Tony Mantler821e5782014-01-06 15:33:43 -080050import com.android.emailcommon.mail.Address;
Andy Huang59e0b182012-08-14 14:32:23 -070051import com.android.mail.FormattedDateBuilder;
Mindy Pereira9b875682012-02-15 18:10:54 -080052import com.android.mail.R;
Andy Huange6c9fb62013-11-15 09:56:20 -080053import com.android.mail.analytics.Analytics;
Jin Cao72953f22014-04-15 18:23:37 -070054import com.android.mail.analytics.AnalyticsTimer;
Andy Huang5ff63742012-03-16 20:30:23 -070055import com.android.mail.browse.ConversationContainer;
Andy Huangadbf3e82012-10-13 13:30:19 -070056import com.android.mail.browse.ConversationContainer.OverlayPosition;
Andrew Sapperstein735a22a2014-07-11 03:57:42 -070057import com.android.mail.browse.ConversationFooterView.ConversationFooterCallbacks;
Andrew Sapperstein8812d3c2013-06-04 17:06:41 -070058import com.android.mail.browse.ConversationMessage;
Andy Huang46dfba62012-04-19 01:47:32 -070059import com.android.mail.browse.ConversationOverlayItem;
Andy Huang7bdc3752012-03-25 17:18:19 -070060import com.android.mail.browse.ConversationViewAdapter;
Andrew Sappersteine2a30e12014-07-02 22:36:56 -070061import com.android.mail.browse.ConversationViewAdapter.ConversationFooterItem;
Andy Huang46dfba62012-04-19 01:47:32 -070062import com.android.mail.browse.ConversationViewAdapter.MessageFooterItem;
Andy Huang7bdc3752012-03-25 17:18:19 -070063import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem;
Andy Huang46dfba62012-04-19 01:47:32 -070064import com.android.mail.browse.ConversationViewAdapter.SuperCollapsedBlockItem;
Andy Huang5ff63742012-03-16 20:30:23 -070065import com.android.mail.browse.ConversationViewHeader;
66import com.android.mail.browse.ConversationWebView;
Andrew Sapperstein8ec43e82013-12-17 18:27:55 -080067import com.android.mail.browse.InlineAttachmentViewIntentBuilderCreator;
68import com.android.mail.browse.InlineAttachmentViewIntentBuilderCreatorHolder;
Andy Huangc1fb9a92013-02-11 13:09:12 -080069import com.android.mail.browse.MailWebView.ContentSizeChangeListener;
Andy Huang7bdc3752012-03-25 17:18:19 -070070import com.android.mail.browse.MessageCursor;
Andrew Sapperstein381c3222014-04-20 12:23:57 -070071import com.android.mail.browse.MessageFooterView;
Andy Huang59e0b182012-08-14 14:32:23 -070072import com.android.mail.browse.MessageHeaderView;
Andy Huangadbf3e82012-10-13 13:30:19 -070073import com.android.mail.browse.ScrollIndicatorsView;
Andy Huang46dfba62012-04-19 01:47:32 -070074import com.android.mail.browse.SuperCollapsedBlock;
Andy Huang0b7ed6f2012-07-25 19:23:26 -070075import com.android.mail.browse.WebViewContextMenu;
Paul Westbrookc42ad5e2013-05-09 16:52:15 -070076import com.android.mail.content.ObjectCursor;
Andrew Sapperstein562c5ba2013-10-09 18:31:50 -070077import com.android.mail.print.PrintUtils;
Mindy Pereira9b875682012-02-15 18:10:54 -080078import com.android.mail.providers.Account;
79import com.android.mail.providers.Conversation;
Andy Huangf70fc402012-02-17 15:37:42 -080080import com.android.mail.providers.Message;
Alice Yangf323c042013-10-30 00:15:02 -070081import com.android.mail.providers.Settings;
Paul Westbrookb8361c92012-09-27 10:57:14 -070082import com.android.mail.providers.UIProvider;
Andy Huangcd5c5ee2012-08-12 19:03:51 -070083import com.android.mail.ui.ConversationViewState.ExpansionState;
Andrew Sapperstein376294b2013-06-06 16:04:26 -070084import com.android.mail.utils.ConversationViewUtils;
Jin Cao52a40d52014-08-22 17:03:40 -070085import com.android.mail.utils.KeyboardUtils;
Paul Westbrookb334c902012-06-25 11:42:46 -070086import com.android.mail.utils.LogTag;
Mindy Pereira9b875682012-02-15 18:10:54 -080087import com.android.mail.utils.LogUtils;
Andy Huang2e9acfe2012-03-15 22:39:36 -070088import com.android.mail.utils.Utils;
Jin Cao8da4dc82014-09-10 13:20:57 -070089import com.android.mail.utils.ViewUtils;
Andy Huang543e7092013-04-22 11:44:56 -070090import com.google.common.collect.ImmutableList;
Andy Huang46dfba62012-04-19 01:47:32 -070091import com.google.common.collect.Lists;
Andy Huang05c70c82013-03-14 15:15:50 -070092import com.google.common.collect.Maps;
Andy Huangb8331b42012-07-16 19:08:53 -070093import com.google.common.collect.Sets;
Andy Huang65fe28f2012-04-06 18:08:53 -070094
Scott Kennedyeb9a4bd2012-11-12 10:33:04 -080095import java.util.ArrayList;
Andy Huang46dfba62012-04-19 01:47:32 -070096import java.util.List;
Andy Huang05c70c82013-03-14 15:15:50 -070097import java.util.Map;
Andy Huangb8331b42012-07-16 19:08:53 -070098import java.util.Set;
Mindy Pereira9b875682012-02-15 18:10:54 -080099
100/**
101 * The conversation view UI component.
102 */
Andrew Sapperstein9f957f32013-07-19 15:18:18 -0700103public class ConversationViewFragment extends AbstractConversationViewFragment implements
Andrew Sapperstein4ddda2f2013-06-10 11:15:38 -0700104 SuperCollapsedBlock.OnClickListener, OnLayoutChangeListener,
Andrew Sapperstein735a22a2014-07-11 03:57:42 -0700105 MessageHeaderView.MessageHeaderViewCallbacks, MessageFooterView.MessageFooterCallbacks,
Jin Caoa7404582014-08-05 14:03:59 -0700106 WebViewContextMenu.Callbacks, ConversationFooterCallbacks, View.OnKeyListener {
Mindy Pereira8e915722012-02-16 14:42:56 -0800107
Paul Westbrookb334c902012-06-25 11:42:46 -0700108 private static final String LOG_TAG = LogTag.getLogTag();
Andy Huang632721e2012-04-11 16:57:26 -0700109 public static final String LAYOUT_TAG = "ConvLayout";
Mindy Pereira9b875682012-02-15 18:10:54 -0800110
Andy Huang9d3fd922012-09-26 22:23:58 -0700111 /**
mindyp1b3cc472012-09-27 11:32:59 -0700112 * Difference in the height of the message header whose details have been expanded/collapsed
113 */
114 private int mDiff = 0;
115
116 /**
Andy Huang9d3fd922012-09-26 22:23:58 -0700117 * Default value for {@link #mLoadWaitReason}. Conversation load will happen immediately.
118 */
119 private final int LOAD_NOW = 0;
120 /**
121 * Value for {@link #mLoadWaitReason} that means we are offscreen and waiting for the visible
122 * conversation to finish loading before beginning our load.
123 * <p>
124 * When this value is set, the fragment should register with {@link ConversationListCallbacks}
125 * to know when the visible conversation is loaded. When it is unset, it should unregister.
126 */
127 private final int LOAD_WAIT_FOR_INITIAL_CONVERSATION = 1;
128 /**
129 * Value for {@link #mLoadWaitReason} used when a conversation is too heavyweight to load at
130 * all when not visible (e.g. requires network fetch, or too complex). Conversation load will
131 * wait until this fragment is visible.
132 */
133 private final int LOAD_WAIT_UNTIL_VISIBLE = 2;
mindyp3bcf1802012-09-09 11:17:00 -0700134
Jin Caoea7a5db2014-09-08 15:24:51 -0700135 // Default scroll distance when the user tries to scroll with up/down
136 private final int DEFAULT_VERTICAL_SCROLL_DISTANCE_PX = 50;
137
Jin Cao0b693382014-08-11 10:46:12 -0700138 // Keyboard navigation
139 private KeyboardNavigationController mNavigationController;
140 // Since we manually control navigation for most of the conversation view due to problems
141 // with two-pane layout but still rely on the system for SOME navigation, we need to keep track
142 // of the view that had focus when KeyEvent.ACTION_DOWN was fired. This is because we only
143 // manually change focus on KeyEvent.ACTION_UP (to prevent holding down the DOWN button and
144 // lagging the app), however, the view in focus might have changed between ACTION_UP and
145 // ACTION_DOWN since the system might have handled the ACTION_DOWN and moved focus.
146 private View mOriginalKeyedView;
147 private int mMaxScreenHeight;
148 private int mTopOfVisibleScreen;
149
Andrew Sapperstein9f957f32013-07-19 15:18:18 -0700150 protected ConversationContainer mConversationContainer;
Mindy Pereira9b875682012-02-15 18:10:54 -0800151
Andrew Sapperstein9f957f32013-07-19 15:18:18 -0700152 protected ConversationWebView mWebView;
Mindy Pereira9b875682012-02-15 18:10:54 -0800153
Jin Caoa7404582014-08-05 14:03:59 -0700154 private ViewGroup mTopmostOverlay;
155
Andrew Sappersteina7fa9bf2014-03-29 13:33:04 -0700156 private ConversationViewProgressController mProgressController;
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700157
James Lemieux0ec03e82014-09-03 14:01:53 -0700158 private ActionableToastBar mNewMessageBar;
159 private ActionableToastBar.ActionClickedListener mNewMessageBarActionListener;
Andy Huang47aa9c92012-07-31 15:37:21 -0700160
Andrew Sapperstein9f957f32013-07-19 15:18:18 -0700161 protected HtmlConversationTemplates mTemplates;
Andy Huangf70fc402012-02-17 15:37:42 -0800162
Andy Huangf70fc402012-02-17 15:37:42 -0800163 private final MailJsBridge mJsBridge = new MailJsBridge();
164
Andrew Sapperstein9f957f32013-07-19 15:18:18 -0700165 protected ConversationViewAdapter mAdapter;
Andy Huang51067132012-03-12 20:08:19 -0700166
Andrew Sappersteinb1d184d2013-08-09 14:14:31 -0700167 protected boolean mViewsCreated;
Mark Wei4071c2f2012-09-26 14:38:38 -0700168 // True if we attempted to render before the views were laid out
169 // We will render immediately once layout is done
170 private boolean mNeedRender;
Andy Huang51067132012-03-12 20:08:19 -0700171
Andy Huang46dfba62012-04-19 01:47:32 -0700172 /**
173 * Temporary string containing the message bodies of the messages within a super-collapsed
174 * block, for one-time use during block expansion. We cannot easily pass the body HTML
175 * into JS without problematic escaping, so hold onto it momentarily and signal JS to fetch it
176 * using {@link MailJsBridge}.
177 */
178 private String mTempBodiesHtml;
179
Andy Huang632721e2012-04-11 16:57:26 -0700180 private int mMaxAutoLoadMessages;
181
Andrew Sapperstein9f957f32013-07-19 15:18:18 -0700182 protected int mSideMarginPx;
Andy Huang02f9d182012-11-28 22:38:02 -0800183
Andy Huang9d3fd922012-09-26 22:23:58 -0700184 /**
185 * If this conversation fragment is not visible, and it's inappropriate to load up front,
186 * this is the reason we are waiting. This flag should be cleared once it's okay to load
187 * the conversation.
188 */
189 private int mLoadWaitReason = LOAD_NOW;
Andy Huang632721e2012-04-11 16:57:26 -0700190
mindyp3bcf1802012-09-09 11:17:00 -0700191 private boolean mEnableContentReadySignal;
Andy Huang28b7aee2012-08-20 20:27:32 -0700192
mindypdde3f9f2012-09-10 17:35:35 -0700193 private ContentSizeChangeListener mWebViewSizeChangeListener;
194
Andy Huange964eee2012-10-02 19:24:58 -0700195 private float mWebViewYPercent;
196
197 /**
198 * Has loadData been called on the WebView yet?
199 */
200 private boolean mWebViewLoadedData;
201
Andy Huang63b3c672012-10-05 19:27:28 -0700202 private long mWebViewLoadStartMs;
203
Andy Huang05c70c82013-03-14 15:15:50 -0700204 private final Map<String, String> mMessageTransforms = Maps.newHashMap();
205
Andy Huang9d3fd922012-09-26 22:23:58 -0700206 private final DataSetObserver mLoadedObserver = new DataSetObserver() {
207 @Override
208 public void onChanged() {
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700209 getHandler().post(new FragmentRunnable("delayedConversationLoad",
210 ConversationViewFragment.this) {
Andy Huang9d3fd922012-09-26 22:23:58 -0700211 @Override
212 public void go() {
213 LogUtils.d(LOG_TAG, "CVF load observer fired, this=%s",
214 ConversationViewFragment.this);
215 handleDelayedConversationLoad();
216 }
217 });
218 }
219 };
Andy Huangf70fc402012-02-17 15:37:42 -0800220
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700221 private final Runnable mOnProgressDismiss = new FragmentRunnable("onProgressDismiss", this) {
Andy Huang7d4746e2012-10-17 17:03:17 -0700222 @Override
223 public void go() {
Scott Kennedy58192e52013-05-08 16:35:57 -0700224 LogUtils.d(LOG_TAG, "onProgressDismiss go() - isUserVisible() = %b", isUserVisible());
Andy Huang7d4746e2012-10-17 17:03:17 -0700225 if (isUserVisible()) {
226 onConversationSeen();
227 }
Andy Huang30bcfe72012-10-18 18:09:03 -0700228 mWebView.onRenderComplete();
Andy Huang7d4746e2012-10-17 17:03:17 -0700229 }
230 };
231
Andy Huangbd544e32012-05-29 15:56:51 -0700232 private static final boolean DEBUG_DUMP_CONVERSATION_HTML = false;
Andy Huang47aa9c92012-07-31 15:37:21 -0700233 private static final boolean DISABLE_OFFSCREEN_LOADING = false;
Andy Huang06c03622012-10-22 18:59:45 -0700234 private static final boolean DEBUG_DUMP_CURSOR_CONTENTS = false;
Andy Huange964eee2012-10-02 19:24:58 -0700235
236 private static final String BUNDLE_KEY_WEBVIEW_Y_PERCENT =
237 ConversationViewFragment.class.getName() + "webview-y-percent";
Andy Huangbd544e32012-05-29 15:56:51 -0700238
Andrew Sapperstein2fd167d2014-01-28 10:07:38 -0800239 private BidiFormatter mBidiFormatter;
Andrew Sapperstein3af481c2013-10-30 10:29:38 -0700240
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800241 /**
Andrew Sapperstein8ec43e82013-12-17 18:27:55 -0800242 * Contains a mapping between inline image attachments and their local message id.
243 */
244 private Map<String, String> mUrlToMessageIdMap;
245
246 /**
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800247 * Constructor needs to be public to handle orientation changes and activity lifecycle events.
248 */
Paul Westbrookf0ea4842013-08-13 16:41:18 -0700249 public ConversationViewFragment() {}
Mindy Pereira9b875682012-02-15 18:10:54 -0800250
251 /**
252 * Creates a new instance of {@link ConversationViewFragment}, initialized
Andy Huang632721e2012-04-11 16:57:26 -0700253 * to display a conversation with other parameters inherited/copied from an existing bundle,
254 * typically one created using {@link #makeBasicArgs}.
255 */
256 public static ConversationViewFragment newInstance(Bundle existingArgs,
257 Conversation conversation) {
258 ConversationViewFragment f = new ConversationViewFragment();
259 Bundle args = new Bundle(existingArgs);
260 args.putParcelable(ARG_CONVERSATION, conversation);
261 f.setArguments(args);
262 return f;
263 }
264
mindypf4fce122012-09-14 15:55:33 -0700265 @Override
Andy Huangadbf3e82012-10-13 13:30:19 -0700266 public void onAccountChanged(Account newAccount, Account oldAccount) {
267 // if overview mode has changed, re-render completely (no need to also update headers)
268 if (isOverviewMode(newAccount) != isOverviewMode(oldAccount)) {
269 setupOverviewMode();
270 final MessageCursor c = getMessageCursor();
271 if (c != null) {
272 renderConversation(c);
273 } else {
274 // Null cursor means this fragment is either waiting to load or in the middle of
275 // loading. Either way, a future render will happen anyway, and the new setting
276 // will take effect when that happens.
277 }
278 return;
279 }
280
mindypf4fce122012-09-14 15:55:33 -0700281 // settings may have been updated; refresh views that are known to
282 // depend on settings
mindypf4fce122012-09-14 15:55:33 -0700283 mAdapter.notifyDataSetChanged();
Andy Huang632721e2012-04-11 16:57:26 -0700284 }
285
Mindy Pereira9b875682012-02-15 18:10:54 -0800286 @Override
287 public void onActivityCreated(Bundle savedInstanceState) {
Andy Huang9d3fd922012-09-26 22:23:58 -0700288 LogUtils.d(LOG_TAG, "IN CVF.onActivityCreated, this=%s visible=%s", this, isUserVisible());
Mindy Pereira9b875682012-02-15 18:10:54 -0800289 super.onActivityCreated(savedInstanceState);
Mark Wei1abfcaf2012-09-27 11:11:07 -0700290
291 if (mActivity == null || mActivity.isFinishing()) {
292 // Activity is finishing, just bail.
293 return;
294 }
295
mindypf4fce122012-09-14 15:55:33 -0700296 Context context = getContext();
297 mTemplates = new HtmlConversationTemplates(context);
Andy Huang59e0b182012-08-14 14:32:23 -0700298
mindypf4fce122012-09-14 15:55:33 -0700299 final FormattedDateBuilder dateBuilder = new FormattedDateBuilder(context);
Andy Huang59e0b182012-08-14 14:32:23 -0700300
Jin Cao0b693382014-08-11 10:46:12 -0700301 mNavigationController = mActivity.getKeyboardNavigationController();
302
Paul Westbrook8081df42012-09-10 15:43:36 -0700303 mAdapter = new ConversationViewAdapter(mActivity, this,
Andrew Sapperstein735a22a2014-07-11 03:57:42 -0700304 getLoaderManager(), this, this, getContactInfoSource(), this, this,
Jin Caoa7404582014-08-05 14:03:59 -0700305 getListController(), this, mAddressCache, dateBuilder, mBidiFormatter, this);
Andy Huang51067132012-03-12 20:08:19 -0700306 mConversationContainer.setOverlayAdapter(mAdapter);
307
Andy Huang59e0b182012-08-14 14:32:23 -0700308 // set up snap header (the adapter usually does this with the other ones)
Andrew Sapperstein85ea6182013-10-14 18:21:08 -0700309 mConversationContainer.getSnapHeader().initialize(
310 this, mAddressCache, this, getContactInfoSource(),
311 mActivity.getAccountController().getVeiledAddressMatcher());
Andy Huangc1fb9a92013-02-11 13:09:12 -0800312
Andrew Sapperstein90eccf92013-08-22 11:53:23 -0700313 final Resources resources = getResources();
314 mMaxAutoLoadMessages = resources.getInteger(R.integer.max_auto_load_messages);
Andy Huang632721e2012-04-11 16:57:26 -0700315
Andrew Sapperstein90eccf92013-08-22 11:53:23 -0700316 mSideMarginPx = resources.getDimensionPixelOffset(
Andy Huang02f9d182012-11-28 22:38:02 -0800317 R.dimen.conversation_message_content_margin_side);
318
Andrew Sapperstein8ec43e82013-12-17 18:27:55 -0800319 mUrlToMessageIdMap = new ArrayMap<String, String>();
320 final InlineAttachmentViewIntentBuilderCreator creator =
321 InlineAttachmentViewIntentBuilderCreatorHolder.
322 getInlineAttachmentViewIntentCreator();
Andy Huang3c6fd442014-03-24 19:56:46 -0700323 final WebViewContextMenu contextMenu = new WebViewContextMenu(getActivity(),
Paul Westbrook91fa0342014-08-19 03:25:17 -0700324 creator.createInlineAttachmentViewIntentBuilder(mAccount,
Andy Huang3c6fd442014-03-24 19:56:46 -0700325 mConversation != null ? mConversation.id : -1));
326 contextMenu.setCallbacks(this);
327 mWebView.setOnCreateContextMenuListener(contextMenu);
Andy Huang0b7ed6f2012-07-25 19:23:26 -0700328
Andy Huangadbf3e82012-10-13 13:30:19 -0700329 // set this up here instead of onCreateView to ensure the latest Account is loaded
330 setupOverviewMode();
331
Andy Huang9d3fd922012-09-26 22:23:58 -0700332 // Defer the call to initLoader with a Handler.
333 // We want to wait until we know which fragments are present and their final visibility
334 // states before going off and doing work. This prevents extraneous loading from occurring
335 // as the ViewPager shifts about before the initial position is set.
336 //
337 // e.g. click on item #10
338 // ViewPager.setAdapter() actually first loads #0 and #1 under the assumption that #0 is
339 // the initial primary item
340 // Then CPC immediately sets the primary item to #10, which tears down #0/#1 and sets up
341 // #9/#10/#11.
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700342 getHandler().post(new FragmentRunnable("showConversation", this) {
Andy Huang9d3fd922012-09-26 22:23:58 -0700343 @Override
344 public void go() {
345 showConversation();
346 }
347 });
Paul Westbrookcebcc642012-08-08 10:06:04 -0700348
Andrew Sapperstein606dbd72013-07-30 19:14:23 -0700349 if (mConversation != null && mConversation.conversationBaseUri != null &&
Andrew Sappersteinf59d01c2014-02-20 10:27:06 -0800350 !Utils.isEmpty(mAccount.accountCookieQueryUri)) {
Paul Westbrookcebcc642012-08-08 10:06:04 -0700351 // Set the cookie for this base url
Andrew Sappersteinf59d01c2014-02-20 10:27:06 -0800352 new SetCookieTask(getContext(), mConversation.conversationBaseUri.toString(),
353 mAccount.accountCookieQueryUri).execute();
Paul Westbrookcebcc642012-08-08 10:06:04 -0700354 }
Jin Cao0b693382014-08-11 10:46:12 -0700355
356 // Find the height of the screen for manually scrolling the webview via keyboard.
357 final Rect screen = new Rect();
358 mActivity.getWindow().getDecorView().getWindowVisibleDisplayFrame(screen);
359 mMaxScreenHeight = screen.bottom;
360 mTopOfVisibleScreen = screen.top + mActivity.getSupportActionBar().getHeight();
Mindy Pereira9b875682012-02-15 18:10:54 -0800361 }
362
Mindy Pereira9b875682012-02-15 18:10:54 -0800363 @Override
Andy Huange964eee2012-10-02 19:24:58 -0700364 public void onCreate(Bundle savedState) {
365 super.onCreate(savedState);
366
Andrew Sappersteinb1d184d2013-08-09 14:14:31 -0700367 mWebViewClient = createConversationWebViewClient();
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700368
Andy Huange964eee2012-10-02 19:24:58 -0700369 if (savedState != null) {
370 mWebViewYPercent = savedState.getFloat(BUNDLE_KEY_WEBVIEW_Y_PERCENT);
371 }
Andrew Sapperstein3af481c2013-10-30 10:29:38 -0700372
Andrew Sapperstein2fd167d2014-01-28 10:07:38 -0800373 mBidiFormatter = BidiFormatter.getInstance();
Andy Huange964eee2012-10-02 19:24:58 -0700374 }
375
Andrew Sappersteinb1d184d2013-08-09 14:14:31 -0700376 protected ConversationWebViewClient createConversationWebViewClient() {
377 return new ConversationWebViewClient(mAccount);
378 }
379
Andy Huange964eee2012-10-02 19:24:58 -0700380 @Override
Mindy Pereira9b875682012-02-15 18:10:54 -0800381 public View onCreateView(LayoutInflater inflater,
382 ViewGroup container, Bundle savedInstanceState) {
Andy Huang632721e2012-04-11 16:57:26 -0700383 View rootView = inflater.inflate(R.layout.conversation_view, container, false);
Andy Huangf70fc402012-02-17 15:37:42 -0800384 mConversationContainer = (ConversationContainer) rootView
385 .findViewById(R.id.conversation_container);
Andy Huang8f187782012-11-06 17:49:25 -0800386 mConversationContainer.setAccountController(this);
Andy Huang47aa9c92012-07-31 15:37:21 -0700387
Jin Caoa7404582014-08-05 14:03:59 -0700388 mTopmostOverlay =
Andrew Sapperstein85ea6182013-10-14 18:21:08 -0700389 (ViewGroup) mConversationContainer.findViewById(R.id.conversation_topmost_overlay);
Jin Caoa7404582014-08-05 14:03:59 -0700390 mTopmostOverlay.setOnKeyListener(this);
391 inflateSnapHeader(mTopmostOverlay, inflater);
Andrew Sapperstein85ea6182013-10-14 18:21:08 -0700392 mConversationContainer.setupSnapHeader();
393
394 setupNewMessageBar();
Andy Huang47aa9c92012-07-31 15:37:21 -0700395
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700396 mProgressController = new ConversationViewProgressController(this, getHandler());
397 mProgressController.instantiateProgressIndicators(rootView);
mindyp3bcf1802012-09-09 11:17:00 -0700398
Andrew Sappersteine2a30e12014-07-02 22:36:56 -0700399 mWebView = (ConversationWebView)
400 mConversationContainer.findViewById(R.id.conversation_webview);
Andy Huangf70fc402012-02-17 15:37:42 -0800401
Andy Huangf70fc402012-02-17 15:37:42 -0800402 mWebView.addJavascriptInterface(mJsBridge, "mail");
mindyp3bcf1802012-09-09 11:17:00 -0700403 // On JB or newer, we use the 'webkitAnimationStart' DOM event to signal load complete
404 // Below JB, try to speed up initial render by having the webview do supplemental draws to
405 // custom a software canvas.
mindypb941fdb2012-09-11 08:28:23 -0700406 // TODO(mindyp):
407 //PAGE READINESS SIGNAL FOR JELLYBEAN AND NEWER
408 // Notify the app on 'webkitAnimationStart' of a simple dummy element with a simple no-op
409 // animation that immediately runs on page load. The app uses this as a signal that the
410 // content is loaded and ready to draw, since WebView delays firing this event until the
411 // layers are composited and everything is ready to draw.
412 // This signal does not seem to be reliable, so just use the old method for now.
Andy Huangf7ac83f2013-07-15 15:48:31 -0700413 final boolean isJBOrLater = Utils.isRunningJellybeanOrLater();
414 final boolean isUserVisible = isUserVisible();
415 mWebView.setUseSoftwareLayer(!isJBOrLater);
416 mEnableContentReadySignal = isJBOrLater && isUserVisible;
417 mWebView.onUserVisibilityChanged(isUserVisible);
Andy Huang17a9cde2012-03-09 18:03:16 -0800418 mWebView.setWebViewClient(mWebViewClient);
Andy Huangc1fb9a92013-02-11 13:09:12 -0800419 final WebChromeClient wcc = new WebChromeClient() {
Andy Huangf70fc402012-02-17 15:37:42 -0800420 @Override
421 public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
Andrew Sapperstein8ec43e82013-12-17 18:27:55 -0800422 if (consoleMessage.messageLevel() == ConsoleMessage.MessageLevel.ERROR) {
Andrew Sapperstein12664d52014-07-09 14:37:47 -0700423 LogUtils.e(LOG_TAG, "JS: %s (%s:%d) f=%s", consoleMessage.message(),
Andrew Sapperstein8ec43e82013-12-17 18:27:55 -0800424 consoleMessage.sourceId(), consoleMessage.lineNumber(),
425 ConversationViewFragment.this);
426 } else {
427 LogUtils.i(LOG_TAG, "JS: %s (%s:%d) f=%s", consoleMessage.message(),
428 consoleMessage.sourceId(), consoleMessage.lineNumber(),
429 ConversationViewFragment.this);
430 }
Andy Huangf70fc402012-02-17 15:37:42 -0800431 return true;
432 }
Andy Huangc1fb9a92013-02-11 13:09:12 -0800433 };
434 mWebView.setWebChromeClient(wcc);
Andy Huangf70fc402012-02-17 15:37:42 -0800435
Andy Huang3233bff2012-03-20 19:38:45 -0700436 final WebSettings settings = mWebView.getSettings();
Andy Huangf70fc402012-02-17 15:37:42 -0800437
Greg Bullockf50aafa2014-03-22 02:17:00 +0100438 final ScrollIndicatorsView scrollIndicators =
Greg Bullock82d9f632014-04-29 10:56:12 +0200439 (ScrollIndicatorsView) rootView.findViewById(R.id.scroll_indicators);
Greg Bullockf50aafa2014-03-22 02:17:00 +0100440 scrollIndicators.setSourceView(mWebView);
Mark Wei56d83852012-09-19 14:28:50 -0700441
Andy Huangf70fc402012-02-17 15:37:42 -0800442 settings.setJavaScriptEnabled(true);
Andy Huangf70fc402012-02-17 15:37:42 -0800443
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700444 ConversationViewUtils.setTextZoom(getResources(), settings);
Andy Huangc319b552012-04-25 19:53:50 -0700445
Andy Huang51067132012-03-12 20:08:19 -0700446 mViewsCreated = true;
Andy Huange964eee2012-10-02 19:24:58 -0700447 mWebViewLoadedData = false;
Andy Huang51067132012-03-12 20:08:19 -0700448
Mindy Pereira9b875682012-02-15 18:10:54 -0800449 return rootView;
450 }
451
Andrew Sapperstein85ea6182013-10-14 18:21:08 -0700452 protected void inflateSnapHeader(ViewGroup topmostOverlay, LayoutInflater inflater) {
453 inflater.inflate(R.layout.conversation_topmost_overlay_items, topmostOverlay, true);
454 }
455
456 protected void setupNewMessageBar() {
James Lemieux0ec03e82014-09-03 14:01:53 -0700457 mNewMessageBar = (ActionableToastBar) mConversationContainer.findViewById(
Andrew Sapperstein85ea6182013-10-14 18:21:08 -0700458 R.id.new_message_notification_bar);
James Lemieux0ec03e82014-09-03 14:01:53 -0700459 mNewMessageBarActionListener = new ActionableToastBar.ActionClickedListener() {
Andrew Sapperstein85ea6182013-10-14 18:21:08 -0700460 @Override
James Lemieux0ec03e82014-09-03 14:01:53 -0700461 public void onActionClicked(Context context) {
Andrew Sapperstein85ea6182013-10-14 18:21:08 -0700462 onNewMessageBarClick();
463 }
James Lemieux0ec03e82014-09-03 14:01:53 -0700464 };
Andrew Sapperstein85ea6182013-10-14 18:21:08 -0700465 }
466
Mindy Pereira9b875682012-02-15 18:10:54 -0800467 @Override
Andy Huangf7ac83f2013-07-15 15:48:31 -0700468 public void onResume() {
469 super.onResume();
470 if (mWebView != null) {
471 mWebView.onResume();
472 }
473 }
474
475 @Override
476 public void onPause() {
477 super.onPause();
478 if (mWebView != null) {
479 mWebView.onPause();
480 }
481 }
482
483 @Override
Mindy Pereira9b875682012-02-15 18:10:54 -0800484 public void onDestroyView() {
Mindy Pereira9b875682012-02-15 18:10:54 -0800485 super.onDestroyView();
Andy Huang46dfba62012-04-19 01:47:32 -0700486 mConversationContainer.setOverlayAdapter(null);
487 mAdapter = null;
Andy Huang9d3fd922012-09-26 22:23:58 -0700488 resetLoadWaiting(); // be sure to unregister any active load observer
Andy Huang51067132012-03-12 20:08:19 -0700489 mViewsCreated = false;
Mindy Pereira9b875682012-02-15 18:10:54 -0800490 }
491
Andy Huange964eee2012-10-02 19:24:58 -0700492 @Override
493 public void onSaveInstanceState(Bundle outState) {
494 super.onSaveInstanceState(outState);
495
496 outState.putFloat(BUNDLE_KEY_WEBVIEW_Y_PERCENT, calculateScrollYPercent());
497 }
498
499 private float calculateScrollYPercent() {
Paul Westbrook1b56a672013-04-19 01:19:05 -0700500 final float p;
501 if (mWebView == null) {
502 // onCreateView hasn't been called, return 0 as the user hasn't scrolled the view.
503 return 0;
504 }
505
506 final int scrollY = mWebView.getScrollY();
507 final int viewH = mWebView.getHeight();
508 final int webH = (int) (mWebView.getContentHeight() * mWebView.getScale());
Andy Huange964eee2012-10-02 19:24:58 -0700509
510 if (webH == 0 || webH <= viewH) {
511 p = 0;
512 } else if (scrollY + viewH >= webH) {
513 // The very bottom is a special case, it acts as a stronger anchor than the scroll top
514 // at that point.
515 p = 1.0f;
516 } else {
517 p = (float) scrollY / webH;
518 }
519 return p;
520 }
521
Andy Huang9d3fd922012-09-26 22:23:58 -0700522 private void resetLoadWaiting() {
523 if (mLoadWaitReason == LOAD_WAIT_FOR_INITIAL_CONVERSATION) {
524 getListController().unregisterConversationLoadedObserver(mLoadedObserver);
525 }
526 mLoadWaitReason = LOAD_NOW;
527 }
528
Andy Huang5ff63742012-03-16 20:30:23 -0700529 @Override
mindypf4fce122012-09-14 15:55:33 -0700530 protected void markUnread() {
Vikram Aggarwald82a31f2013-02-05 15:03:00 -0800531 super.markUnread();
Andy Huang839ada22012-07-20 15:48:40 -0700532 // Ignore unsafe calls made after a fragment is detached from an activity
533 final ControllableActivity activity = (ControllableActivity) getActivity();
534 if (activity == null) {
535 LogUtils.w(LOG_TAG, "ignoring markUnread for conv=%s", mConversation.id);
536 return;
537 }
538
Andy Huang28e31e22012-07-26 16:33:15 -0700539 if (mViewState == null) {
540 LogUtils.i(LOG_TAG, "ignoring markUnread for conv with no view state (%d)",
541 mConversation.id);
542 return;
543 }
Andy Huang839ada22012-07-20 15:48:40 -0700544 activity.getConversationUpdater().markConversationMessagesUnread(mConversation,
Vikram Aggarwal4a878b62012-07-31 15:09:25 -0700545 mViewState.getUnreadMessageUris(), mViewState.getConversationInfo());
Andy Huang839ada22012-07-20 15:48:40 -0700546 }
547
mindypf4fce122012-09-14 15:55:33 -0700548 @Override
549 public void onUserVisibleHintChanged() {
Andy Huang9d3fd922012-09-26 22:23:58 -0700550 final boolean userVisible = isUserVisible();
Scott Kennedy58192e52013-05-08 16:35:57 -0700551 LogUtils.d(LOG_TAG, "ConversationViewFragment#onUserVisibleHintChanged(), userVisible = %b",
552 userVisible);
Andy Huang9d3fd922012-09-26 22:23:58 -0700553
554 if (!userVisible) {
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700555 mProgressController.dismissLoadingStatus();
Andy Huang9d3fd922012-09-26 22:23:58 -0700556 } else if (mViewsCreated) {
Andy Huange6c9fb62013-11-15 09:56:20 -0800557 String loadTag = null;
Andy Huangeeb4a352013-11-21 10:56:27 -0800558 final boolean isInitialLoading;
559 if (mActivity != null) {
560 isInitialLoading = mActivity.getConversationUpdater()
Andy Huange6c9fb62013-11-15 09:56:20 -0800561 .isInitialConversationLoading();
Andy Huangeeb4a352013-11-21 10:56:27 -0800562 } else {
563 isInitialLoading = true;
564 }
Andy Huange6c9fb62013-11-15 09:56:20 -0800565
Andy Huang9d3fd922012-09-26 22:23:58 -0700566 if (getMessageCursor() != null) {
567 LogUtils.d(LOG_TAG, "Fragment is now user-visible, onConversationSeen: %s", this);
Andy Huange6c9fb62013-11-15 09:56:20 -0800568 if (!isInitialLoading) {
569 loadTag = "preloaded";
570 }
Andy Huang9d3fd922012-09-26 22:23:58 -0700571 onConversationSeen();
572 } else if (isLoadWaiting()) {
573 LogUtils.d(LOG_TAG, "Fragment is now user-visible, showing conversation: %s", this);
Andy Huange6c9fb62013-11-15 09:56:20 -0800574 if (!isInitialLoading) {
575 loadTag = "load_deferred";
576 }
Andy Huang9d3fd922012-09-26 22:23:58 -0700577 handleDelayedConversationLoad();
578 }
Andy Huange6c9fb62013-11-15 09:56:20 -0800579
580 if (loadTag != null) {
581 // pager swipes are visibility transitions to 'visible' except during initial
582 // pager load on A) enter conversation mode B) rotate C) 2-pane conv-mode list-tap
583 Analytics.getInstance().sendEvent("pager_swipe", loadTag,
584 getCurrentFolderTypeDesc(), 0);
585 }
Andy Huang632721e2012-04-11 16:57:26 -0700586 }
Andy Huang632721e2012-04-11 16:57:26 -0700587
Andy Huang30bcfe72012-10-18 18:09:03 -0700588 if (mWebView != null) {
589 mWebView.onUserVisibilityChanged(userVisible);
590 }
Andy Huangf8cf5462012-10-17 18:29:14 -0700591 }
592
Andy Huang9d3fd922012-09-26 22:23:58 -0700593 /**
594 * Will either call initLoader now to begin loading, or set {@link #mLoadWaitReason} and do
595 * nothing (in which case you should later call {@link #handleDelayedConversationLoad()}).
596 */
Mindy Pereira9b875682012-02-15 18:10:54 -0800597 private void showConversation() {
Andy Huang9d3fd922012-09-26 22:23:58 -0700598 final int reason;
599
600 if (isUserVisible()) {
601 LogUtils.i(LOG_TAG,
602 "SHOWCONV: CVF is user-visible, immediately loading conversation (%s)", this);
603 reason = LOAD_NOW;
Andy Huang243c2362013-03-01 17:50:35 -0800604 timerMark("CVF.showConversation");
Andy Huang9d3fd922012-09-26 22:23:58 -0700605 } else {
606 final boolean disableOffscreenLoading = DISABLE_OFFSCREEN_LOADING
Alice Yang0b8c0802013-09-16 14:35:18 -0700607 || Utils.isLowRamDevice(getContext())
Andrew Sapperstein606dbd72013-07-30 19:14:23 -0700608 || (mConversation != null && (mConversation.isRemote
609 || mConversation.getNumMessages() > mMaxAutoLoadMessages));
Andy Huang9d3fd922012-09-26 22:23:58 -0700610
611 // When not visible, we should not immediately load if either this conversation is
612 // too heavyweight, or if the main/initial conversation is busy loading.
613 if (disableOffscreenLoading) {
614 reason = LOAD_WAIT_UNTIL_VISIBLE;
615 LogUtils.i(LOG_TAG, "SHOWCONV: CVF waiting until visible to load (%s)", this);
616 } else if (getListController().isInitialConversationLoading()) {
617 reason = LOAD_WAIT_FOR_INITIAL_CONVERSATION;
618 LogUtils.i(LOG_TAG, "SHOWCONV: CVF waiting for initial to finish (%s)", this);
619 getListController().registerConversationLoadedObserver(mLoadedObserver);
620 } else {
621 LogUtils.i(LOG_TAG,
622 "SHOWCONV: CVF is not visible, but no reason to wait. loading now. (%s)",
623 this);
624 reason = LOAD_NOW;
625 }
Andy Huang632721e2012-04-11 16:57:26 -0700626 }
Andy Huang9d3fd922012-09-26 22:23:58 -0700627
628 mLoadWaitReason = reason;
629 if (mLoadWaitReason == LOAD_NOW) {
630 startConversationLoad();
631 }
632 }
633
634 private void handleDelayedConversationLoad() {
635 resetLoadWaiting();
636 startConversationLoad();
637 }
638
639 private void startConversationLoad() {
mindyp3bcf1802012-09-09 11:17:00 -0700640 mWebView.setVisibility(View.VISIBLE);
Andrew Sapperstein606dbd72013-07-30 19:14:23 -0700641 loadContent();
mindyp3bcf1802012-09-09 11:17:00 -0700642 // TODO(mindyp): don't show loading status for a previously rendered
643 // conversation. Ielieve this is better done by making sure don't show loading status
644 // until XX ms have passed without loading completed.
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700645 mProgressController.showLoadingStatus(isUserVisible());
Mindy Pereira8e915722012-02-16 14:42:56 -0800646 }
647
Andrew Sapperstein606dbd72013-07-30 19:14:23 -0700648 /**
649 * Can be overridden in case a subclass needs to load something other than
650 * the messages of a conversation.
651 */
652 protected void loadContent() {
653 getLoaderManager().initLoader(MESSAGE_LOADER, Bundle.EMPTY, getMessageLoaderCallbacks());
654 }
655
Andy Huang7d4746e2012-10-17 17:03:17 -0700656 private void revealConversation() {
Andy Huang243c2362013-03-01 17:50:35 -0800657 timerMark("revealing conversation");
Andrew Sapperstein376294b2013-06-06 16:04:26 -0700658 mProgressController.dismissLoadingStatus(mOnProgressDismiss);
Jin Cao72953f22014-04-15 18:23:37 -0700659 if (isUserVisible()) {
660 AnalyticsTimer.getInstance().logDuration(AnalyticsTimer.OPEN_CONV_VIEW_FROM_LIST,
661 true /* isDestructive */, "open_conversation", "from_list", null);
662 }
Andy Huang7d4746e2012-10-17 17:03:17 -0700663 }
664
Andy Huang9d3fd922012-09-26 22:23:58 -0700665 private boolean isLoadWaiting() {
666 return mLoadWaitReason != LOAD_NOW;
667 }
668
Andy Huang51067132012-03-12 20:08:19 -0700669 private void renderConversation(MessageCursor messageCursor) {
mindyp3bcf1802012-09-09 11:17:00 -0700670 final String convHtml = renderMessageBodies(messageCursor, mEnableContentReadySignal);
Andy Huang243c2362013-03-01 17:50:35 -0800671 timerMark("rendered conversation");
Andy Huangbd544e32012-05-29 15:56:51 -0700672
673 if (DEBUG_DUMP_CONVERSATION_HTML) {
674 java.io.FileWriter fw = null;
675 try {
Andrew Sappersteinf1566b12013-09-19 15:34:26 -0700676 fw = new java.io.FileWriter(getSdCardFilePath());
Andy Huangbd544e32012-05-29 15:56:51 -0700677 fw.write(convHtml);
678 } catch (java.io.IOException e) {
679 e.printStackTrace();
680 } finally {
681 if (fw != null) {
682 try {
683 fw.close();
684 } catch (java.io.IOException e) {
685 e.printStackTrace();
686 }
687 }
688 }
689 }
690
Andy Huange964eee2012-10-02 19:24:58 -0700691 // save off existing scroll position before re-rendering
692 if (mWebViewLoadedData) {
693 mWebViewYPercent = calculateScrollYPercent();
694 }
695
Andy Huangbd544e32012-05-29 15:56:51 -0700696 mWebView.loadDataWithBaseURL(mBaseUri, convHtml, "text/html", "utf-8", null);
Andy Huange964eee2012-10-02 19:24:58 -0700697 mWebViewLoadedData = true;
Andy Huang63b3c672012-10-05 19:27:28 -0700698 mWebViewLoadStartMs = SystemClock.uptimeMillis();
Andy Huang51067132012-03-12 20:08:19 -0700699 }
700
Andrew Sappersteinf1566b12013-09-19 15:34:26 -0700701 protected String getSdCardFilePath() {
702 return "/sdcard/conv" + mConversation.id + ".html";
703 }
704
Andy Huang7bdc3752012-03-25 17:18:19 -0700705 /**
706 * Populate the adapter with overlay views (message headers, super-collapsed blocks, a
707 * conversation header), and return an HTML document with spacer divs inserted for all overlays.
708 *
709 */
Andrew Sapperstein9f957f32013-07-19 15:18:18 -0700710 protected String renderMessageBodies(MessageCursor messageCursor,
mindyp3bcf1802012-09-09 11:17:00 -0700711 boolean enableContentReadySignal) {
Andy Huangf70fc402012-02-17 15:37:42 -0800712 int pos = -1;
Andy Huang632721e2012-04-11 16:57:26 -0700713
Andy Huang1ee96b22012-08-24 20:19:53 -0700714 LogUtils.d(LOG_TAG, "IN renderMessageBodies, fragment=%s", this);
Andy Huang7bdc3752012-03-25 17:18:19 -0700715 boolean allowNetworkImages = false;
716
Andy Huangc7543572012-04-03 15:34:29 -0700717 // TODO: re-use any existing adapter item state (expanded, details expanded, show pics)
Andy Huang28b7aee2012-08-20 20:27:32 -0700718
Andy Huang7bdc3752012-03-25 17:18:19 -0700719 // Walk through the cursor and build up an overlay adapter as you go.
720 // Each overlay has an entry in the adapter for easy scroll handling in the container.
721 // Items are not necessarily 1:1 in cursor and adapter because of super-collapsed blocks.
722 // When adding adapter items, also add their heights to help the container later determine
723 // overlay dimensions.
724
Andy Huangdb620fe2012-08-24 15:45:28 -0700725 // When re-rendering, prevent ConversationContainer from laying out overlays until after
726 // the new spacers are positioned by WebView.
727 mConversationContainer.invalidateSpacerGeometry();
728
Andy Huang7bdc3752012-03-25 17:18:19 -0700729 mAdapter.clear();
730
Andy Huang47aa9c92012-07-31 15:37:21 -0700731 // re-evaluate the message parts of the view state, since the messages may have changed
732 // since the previous render
733 final ConversationViewState prevState = mViewState;
734 mViewState = new ConversationViewState(prevState);
735
Andy Huang5ff63742012-03-16 20:30:23 -0700736 // N.B. the units of height for spacers are actually dp and not px because WebView assumes
Andy Huang2e9acfe2012-03-15 22:39:36 -0700737 // a pixel is an mdpi pixel, unless you set device-dpi.
Andy Huang5ff63742012-03-16 20:30:23 -0700738
Andy Huang7bdc3752012-03-25 17:18:19 -0700739 // add a single conversation header item
740 final int convHeaderPos = mAdapter.addConversationHeader(mConversation);
Andy Huang23014702012-07-09 12:50:36 -0700741 final int convHeaderPx = measureOverlayHeight(convHeaderPos);
Andy Huang5ff63742012-03-16 20:30:23 -0700742
Andy Huang4dc73232014-02-04 19:58:57 -0800743 mTemplates.startConversation(mWebView.getViewportWidth(),
744 mWebView.screenPxToWebPx(mSideMarginPx), mWebView.screenPxToWebPx(convHeaderPx));
Andy Huang3233bff2012-03-20 19:38:45 -0700745
Andy Huang46dfba62012-04-19 01:47:32 -0700746 int collapsedStart = -1;
Andy Huang839ada22012-07-20 15:48:40 -0700747 ConversationMessage prevCollapsedMsg = null;
Andrew Sappersteine8221482013-10-02 18:14:58 -0700748
Andrew Sapperstein8ec43e82013-12-17 18:27:55 -0800749 final boolean alwaysShowImages = shouldAlwaysShowImages();
Alice Yangf323c042013-10-30 00:15:02 -0700750
Andrew Sappersteine8221482013-10-02 18:14:58 -0700751 boolean prevSafeForImages = alwaysShowImages;
Andy Huang46dfba62012-04-19 01:47:32 -0700752
Andrew Sapperstein735a22a2014-07-11 03:57:42 -0700753 boolean hasDraft = false;
Andy Huangf70fc402012-02-17 15:37:42 -0800754 while (messageCursor.moveToPosition(++pos)) {
Andy Huang839ada22012-07-20 15:48:40 -0700755 final ConversationMessage msg = messageCursor.getMessage();
Andy Huang46dfba62012-04-19 01:47:32 -0700756
Andrew Sappersteine8221482013-10-02 18:14:58 -0700757 final boolean safeForImages = alwaysShowImages ||
Scott Kennedy20273842012-11-07 11:16:21 -0800758 msg.alwaysShowImages || prevState.getShouldShowImages(msg);
Andy Huang3233bff2012-03-20 19:38:45 -0700759 allowNetworkImages |= safeForImages;
Andy Huang24055282012-03-27 17:37:06 -0700760
Paul Westbrook08098ec2012-08-12 15:30:28 -0700761 final Integer savedExpanded = prevState.getExpansionState(msg);
762 final int expandedState;
Andy Huang839ada22012-07-20 15:48:40 -0700763 if (savedExpanded != null) {
Andy Huang1ee96b22012-08-24 20:19:53 -0700764 if (ExpansionState.isSuperCollapsed(savedExpanded) && messageCursor.isLast()) {
765 // override saved state when this is now the new last message
766 // this happens to the second-to-last message when you discard a draft
767 expandedState = ExpansionState.EXPANDED;
768 } else {
769 expandedState = savedExpanded;
770 }
Andy Huang839ada22012-07-20 15:48:40 -0700771 } else {
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700772 // new messages that are not expanded default to being eligible for super-collapse
Andrew Sapperstein57f82d22014-09-08 18:49:33 -0700773 if (msg.starred || !msg.read || messageCursor.isLast()) {
Andrew Sapperstein605dcfc2014-07-03 14:43:35 -0700774 expandedState = ExpansionState.EXPANDED;
775 } else if (messageCursor.isFirst()) {
776 expandedState = ExpansionState.COLLAPSED;
777 } else {
778 expandedState = ExpansionState.SUPER_COLLAPSED;
Andrew Sapperstein735a22a2014-07-11 03:57:42 -0700779 hasDraft |= msg.isDraft();
Andrew Sapperstein605dcfc2014-07-03 14:43:35 -0700780 }
Andy Huang839ada22012-07-20 15:48:40 -0700781 }
Scott Kennedy20273842012-11-07 11:16:21 -0800782 mViewState.setShouldShowImages(msg, prevState.getShouldShowImages(msg));
Paul Westbrook08098ec2012-08-12 15:30:28 -0700783 mViewState.setExpansionState(msg, expandedState);
Andy Huangc7543572012-04-03 15:34:29 -0700784
Andy Huang839ada22012-07-20 15:48:40 -0700785 // save off "read" state from the cursor
786 // later, the view may not match the cursor (e.g. conversation marked read on open)
Andy Huang423bea22012-08-21 12:00:49 -0700787 // however, if a previous state indicated this message was unread, trust that instead
788 // so "mark unread" marks all originally unread messages
789 mViewState.setReadState(msg, msg.read && !prevState.isUnread(msg));
Andy Huang839ada22012-07-20 15:48:40 -0700790
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700791 // We only want to consider this for inclusion in the super collapsed block if
792 // 1) The we don't have previous state about this message (The first time that the
793 // user opens a conversation)
794 // 2) The previously saved state for this message indicates that this message is
795 // in the super collapsed block.
796 if (ExpansionState.isSuperCollapsed(expandedState)) {
797 // contribute to a super-collapsed block that will be emitted just before the
798 // next expanded header
799 if (collapsedStart < 0) {
800 collapsedStart = pos;
Andy Huang46dfba62012-04-19 01:47:32 -0700801 }
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700802 prevCollapsedMsg = msg;
803 prevSafeForImages = safeForImages;
Andrew Sapperstein7dc7fa02013-05-08 16:39:31 -0700804
805 // This line puts the from address in the address cache so that
806 // we get the sender image for it if it's in a super-collapsed block.
807 getAddress(msg.getFrom());
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700808 continue;
Andy Huang46dfba62012-04-19 01:47:32 -0700809 }
Andy Huang24055282012-03-27 17:37:06 -0700810
Andy Huang46dfba62012-04-19 01:47:32 -0700811 // resolve any deferred decisions on previous collapsed items
812 if (collapsedStart >= 0) {
813 if (pos - collapsedStart == 1) {
Andrew Sappersteincee3c902013-07-31 10:52:02 -0700814 // Special-case for a single collapsed message: no need to super-collapse it.
Andrew Sappersteine2a30e12014-07-02 22:36:56 -0700815 renderMessage(prevCollapsedMsg, false /* expanded */, prevSafeForImages);
Andy Huang46dfba62012-04-19 01:47:32 -0700816 } else {
Andrew Sapperstein735a22a2014-07-11 03:57:42 -0700817 renderSuperCollapsedBlock(collapsedStart, pos - 1, hasDraft);
Andy Huang46dfba62012-04-19 01:47:32 -0700818 }
Andrew Sapperstein735a22a2014-07-11 03:57:42 -0700819 hasDraft = false; // reset hasDraft
Andy Huang46dfba62012-04-19 01:47:32 -0700820 prevCollapsedMsg = null;
821 collapsedStart = -1;
822 }
Andy Huang7bdc3752012-03-25 17:18:19 -0700823
Andrew Sappersteine2a30e12014-07-02 22:36:56 -0700824 renderMessage(msg, ExpansionState.isExpanded(expandedState), safeForImages);
Mindy Pereira9b875682012-02-15 18:10:54 -0800825 }
Andy Huang3233bff2012-03-20 19:38:45 -0700826
Andrew Sappersteine2a30e12014-07-02 22:36:56 -0700827 final MessageHeaderItem lastHeaderItem = getLastMessageHeaderItem();
828 final int convFooterPos = mAdapter.addConversationFooter(lastHeaderItem);
829 final int convFooterPx = measureOverlayHeight(convFooterPos);
830
Andy Huang3233bff2012-03-20 19:38:45 -0700831 mWebView.getSettings().setBlockNetworkImage(!allowNetworkImages);
832
Andrew Sapperstein2fc67302013-04-29 18:24:56 -0700833 final boolean applyTransforms = shouldApplyTransforms();
Andy Huang57f354c2013-04-11 17:23:40 -0700834
Andy Huangc1fb9a92013-02-11 13:09:12 -0800835 // If the conversation has specified a base uri, use it here, otherwise use mBaseUri
Andrew Sapperstein2b7cf142014-08-19 21:33:32 -0700836 return mTemplates.endConversation(mWebView.screenPxToWebPx(convFooterPx), mBaseUri,
Andrew Sappersteine2a30e12014-07-02 22:36:56 -0700837 mConversation.getBaseUri(mBaseUri),
Andy Huang2160d532014-02-10 15:43:14 -0800838 mWebView.getViewportWidth(), mWebView.getWidthInDp(mSideMarginPx),
839 enableContentReadySignal, isOverviewMode(mAccount), applyTransforms,
840 applyTransforms);
Mindy Pereira9b875682012-02-15 18:10:54 -0800841 }
Mindy Pereira674afa42012-02-17 14:05:24 -0800842
Andrew Sappersteine2a30e12014-07-02 22:36:56 -0700843 private MessageHeaderItem getLastMessageHeaderItem() {
844 final int count = mAdapter.getCount();
845 if (count < 3) {
846 LogUtils.wtf(LOG_TAG, "not enough items in the adapter. count: %s", count);
847 return null;
848 }
849 return (MessageHeaderItem) mAdapter.getItem(count - 2);
850 }
851
Andrew Sapperstein735a22a2014-07-11 03:57:42 -0700852 private void renderSuperCollapsedBlock(int start, int end, boolean hasDraft) {
853 final int blockPos = mAdapter.addSuperCollapsedBlock(start, end, hasDraft);
Andy Huang23014702012-07-09 12:50:36 -0700854 final int blockPx = measureOverlayHeight(blockPos);
855 mTemplates.appendSuperCollapsedHtml(start, mWebView.screenPxToWebPx(blockPx));
Andy Huang46dfba62012-04-19 01:47:32 -0700856 }
857
Andrew Sappersteine2a30e12014-07-02 22:36:56 -0700858 private void renderMessage(ConversationMessage msg, boolean expanded, boolean safeForImages) {
Andrew Sapperstein14f93742013-07-25 14:29:56 -0700859
Scott Kennedy20273842012-11-07 11:16:21 -0800860 final int headerPos = mAdapter.addMessageHeader(msg, expanded,
861 mViewState.getShouldShowImages(msg));
Andy Huang46dfba62012-04-19 01:47:32 -0700862 final MessageHeaderItem headerItem = (MessageHeaderItem) mAdapter.getItem(headerPos);
863
864 final int footerPos = mAdapter.addMessageFooter(headerItem);
865
866 // Measure item header and footer heights to allocate spacers in HTML
867 // But since the views themselves don't exist yet, render each item temporarily into
868 // a host view for measurement.
Andy Huang23014702012-07-09 12:50:36 -0700869 final int headerPx = measureOverlayHeight(headerPos);
870 final int footerPx = measureOverlayHeight(footerPos);
Andy Huang46dfba62012-04-19 01:47:32 -0700871
Andy Huang256b35c2012-08-22 15:19:13 -0700872 mTemplates.appendMessageHtml(msg, expanded, safeForImages,
Andy Huang23014702012-07-09 12:50:36 -0700873 mWebView.screenPxToWebPx(headerPx), mWebView.screenPxToWebPx(footerPx));
Andy Huang243c2362013-03-01 17:50:35 -0800874 timerMark("rendered message");
Andy Huang46dfba62012-04-19 01:47:32 -0700875 }
876
877 private String renderCollapsedHeaders(MessageCursor cursor,
878 SuperCollapsedBlockItem blockToReplace) {
879 final List<ConversationOverlayItem> replacements = Lists.newArrayList();
880
881 mTemplates.reset();
882
Alice Yangf323c042013-10-30 00:15:02 -0700883 final boolean alwaysShowImages = (mAccount != null) &&
884 (mAccount.settings.showImages == Settings.ShowImages.ALWAYS);
Andrew Sappersteine8221482013-10-02 18:14:58 -0700885
Mark Wei2b24e992012-09-10 16:40:07 -0700886 // In devices with non-integral density multiplier, screen pixels translate to non-integral
887 // web pixels. Keep track of the error that occurs when we cast all heights to int
888 float error = 0f;
Andrew Sapperstein14f93742013-07-25 14:29:56 -0700889 boolean first = true;
Andy Huang46dfba62012-04-19 01:47:32 -0700890 for (int i = blockToReplace.getStart(), end = blockToReplace.getEnd(); i <= end; i++) {
891 cursor.moveToPosition(i);
Andy Huang839ada22012-07-20 15:48:40 -0700892 final ConversationMessage msg = cursor.getMessage();
Andrew Sapperstein14f93742013-07-25 14:29:56 -0700893
Andrew Sapperstein4ddda2f2013-06-10 11:15:38 -0700894 final MessageHeaderItem header = ConversationViewAdapter.newMessageHeaderItem(
Andrew Sapperstein14f93742013-07-25 14:29:56 -0700895 mAdapter, mAdapter.getDateBuilder(), msg, false /* expanded */,
Andrew Sappersteine8221482013-10-02 18:14:58 -0700896 alwaysShowImages || mViewState.getShouldShowImages(msg));
Andrew Sapperstein381c3222014-04-20 12:23:57 -0700897 final MessageFooterItem footer = mAdapter.newMessageFooterItem(mAdapter, header);
Andy Huang46dfba62012-04-19 01:47:32 -0700898
Andy Huang23014702012-07-09 12:50:36 -0700899 final int headerPx = measureOverlayHeight(header);
900 final int footerPx = measureOverlayHeight(footer);
Mark Wei2b24e992012-09-10 16:40:07 -0700901 error += mWebView.screenPxToWebPxError(headerPx)
Andrew Sapperstein59ccec32014-06-18 17:22:49 -0700902 + mWebView.screenPxToWebPxError(footerPx);
Mark Wei2b24e992012-09-10 16:40:07 -0700903
904 // When the error becomes greater than 1 pixel, make the next header 1 pixel taller
905 int correction = 0;
906 if (error >= 1) {
907 correction = 1;
908 error -= 1;
909 }
Andy Huang46dfba62012-04-19 01:47:32 -0700910
Andrew Sappersteine8221482013-10-02 18:14:58 -0700911 mTemplates.appendMessageHtml(msg, false /* expanded */,
912 alwaysShowImages || msg.alwaysShowImages,
Mark Wei2b24e992012-09-10 16:40:07 -0700913 mWebView.screenPxToWebPx(headerPx) + correction,
914 mWebView.screenPxToWebPx(footerPx));
Andy Huang46dfba62012-04-19 01:47:32 -0700915 replacements.add(header);
916 replacements.add(footer);
Andy Huang839ada22012-07-20 15:48:40 -0700917
Paul Westbrook08098ec2012-08-12 15:30:28 -0700918 mViewState.setExpansionState(msg, ExpansionState.COLLAPSED);
Andy Huang46dfba62012-04-19 01:47:32 -0700919 }
920
921 mAdapter.replaceSuperCollapsedBlock(blockToReplace, replacements);
Andy Huang06c03622012-10-22 18:59:45 -0700922 mAdapter.notifyDataSetChanged();
Andy Huang46dfba62012-04-19 01:47:32 -0700923
924 return mTemplates.emit();
925 }
926
Andrew Sapperstein9f957f32013-07-19 15:18:18 -0700927 protected int measureOverlayHeight(int position) {
Andy Huang46dfba62012-04-19 01:47:32 -0700928 return measureOverlayHeight(mAdapter.getItem(position));
929 }
930
Andy Huang7bdc3752012-03-25 17:18:19 -0700931 /**
Andy Huangb8331b42012-07-16 19:08:53 -0700932 * Measure the height of an adapter view by rendering an adapter item into a temporary
Andy Huang46dfba62012-04-19 01:47:32 -0700933 * host view, and asking the view to immediately measure itself. This method will reuse
Andy Huang7bdc3752012-03-25 17:18:19 -0700934 * a previous adapter view from {@link ConversationContainer}'s scrap views if one was generated
935 * earlier.
936 * <p>
Andy Huang46dfba62012-04-19 01:47:32 -0700937 * After measuring the height, this method also saves the height in the
938 * {@link ConversationOverlayItem} for later use in overlay positioning.
Andy Huang7bdc3752012-03-25 17:18:19 -0700939 *
Andy Huang46dfba62012-04-19 01:47:32 -0700940 * @param convItem adapter item with data to render and measure
Andy Huang23014702012-07-09 12:50:36 -0700941 * @return height of the rendered view in screen px
Andy Huang7bdc3752012-03-25 17:18:19 -0700942 */
Andrew Sappersteinafaab172014-08-07 18:41:15 -0700943 private int measureOverlayHeight(ConversationOverlayItem convItem) {
Andy Huang7bdc3752012-03-25 17:18:19 -0700944 final int type = convItem.getType();
945
946 final View convertView = mConversationContainer.getScrapView(type);
Andy Huangb8331b42012-07-16 19:08:53 -0700947 final View hostView = mAdapter.getView(convItem, convertView, mConversationContainer,
948 true /* measureOnly */);
Andy Huang7bdc3752012-03-25 17:18:19 -0700949 if (convertView == null) {
950 mConversationContainer.addScrapView(type, hostView);
951 }
952
Andy Huang9875bb42012-04-04 20:36:21 -0700953 final int heightPx = mConversationContainer.measureOverlay(hostView);
Andy Huang7bdc3752012-03-25 17:18:19 -0700954 convItem.setHeight(heightPx);
Andy Huang9875bb42012-04-04 20:36:21 -0700955 convItem.markMeasurementValid();
Andy Huang7bdc3752012-03-25 17:18:19 -0700956
Andy Huang23014702012-07-09 12:50:36 -0700957 return heightPx;
Andy Huang7bdc3752012-03-25 17:18:19 -0700958 }
959
Andy Huang5ff63742012-03-16 20:30:23 -0700960 @Override
961 public void onConversationViewHeaderHeightChange(int newHeight) {
Mark Weiab2d9982012-09-25 13:06:17 -0700962 final int h = mWebView.screenPxToWebPx(newHeight);
963
964 mWebView.loadUrl(String.format("javascript:setConversationHeaderSpacerHeight(%s);", h));
Andy Huang5ff63742012-03-16 20:30:23 -0700965 }
966
Andy Huang3233bff2012-03-20 19:38:45 -0700967 // END conversation header callbacks
968
Andrew Sapperstein735a22a2014-07-11 03:57:42 -0700969 // START conversation footer callbacks
970
971 @Override
972 public void onConversationFooterHeightChange(int newHeight) {
973 final int h = mWebView.screenPxToWebPx(newHeight);
974
975 mWebView.loadUrl(String.format("javascript:setConversationFooterSpacerHeight(%s);", h));
976 }
977
978 // END conversation footer callbacks
979
Andy Huang3233bff2012-03-20 19:38:45 -0700980 // START message header callbacks
981 @Override
Andy Huangc7543572012-04-03 15:34:29 -0700982 public void setMessageSpacerHeight(MessageHeaderItem item, int newSpacerHeightPx) {
983 mConversationContainer.invalidateSpacerGeometry();
984
985 // update message HTML spacer height
Andy Huang23014702012-07-09 12:50:36 -0700986 final int h = mWebView.screenPxToWebPx(newSpacerHeightPx);
987 LogUtils.i(LAYOUT_TAG, "setting HTML spacer h=%dwebPx (%dscreenPx)", h,
988 newSpacerHeightPx);
Vikram Aggarwal5349ce12012-09-24 14:12:40 -0700989 mWebView.loadUrl(String.format("javascript:setMessageHeaderSpacerHeight('%s', %s);",
Andy Huang014ea4c2012-09-25 14:50:54 -0700990 mTemplates.getMessageDomId(item.getMessage()), h));
Andy Huang3233bff2012-03-20 19:38:45 -0700991 }
992
993 @Override
Andrew Sapperstein59ccec32014-06-18 17:22:49 -0700994 public void setMessageExpanded(MessageHeaderItem item, int newSpacerHeightPx) {
Andy Huangc7543572012-04-03 15:34:29 -0700995 mConversationContainer.invalidateSpacerGeometry();
996
997 // show/hide the HTML message body and update the spacer height
Andy Huang23014702012-07-09 12:50:36 -0700998 final int h = mWebView.screenPxToWebPx(newSpacerHeightPx);
999 LogUtils.i(LAYOUT_TAG, "setting HTML spacer expanded=%s h=%dwebPx (%dscreenPx)",
1000 item.isExpanded(), h, newSpacerHeightPx);
Andrew Sapperstein59ccec32014-06-18 17:22:49 -07001001 mWebView.loadUrl(String.format("javascript:setMessageBodyVisible('%s', %s, %s);",
1002 mTemplates.getMessageDomId(item.getMessage()), item.isExpanded(), h));
Andy Huang839ada22012-07-20 15:48:40 -07001003
Andy Huang014ea4c2012-09-25 14:50:54 -07001004 mViewState.setExpansionState(item.getMessage(),
Paul Westbrook08098ec2012-08-12 15:30:28 -07001005 item.isExpanded() ? ExpansionState.EXPANDED : ExpansionState.COLLAPSED);
Andy Huang3233bff2012-03-20 19:38:45 -07001006 }
1007
1008 @Override
Scott Kennedyeb9a4bd2012-11-12 10:33:04 -08001009 public void showExternalResources(final Message msg) {
Scott Kennedy20273842012-11-07 11:16:21 -08001010 mViewState.setShouldShowImages(msg, true);
Andy Huang3233bff2012-03-20 19:38:45 -07001011 mWebView.getSettings().setBlockNetworkImage(false);
Scott Kennedyeb9a4bd2012-11-12 10:33:04 -08001012 mWebView.loadUrl("javascript:unblockImages(['" + mTemplates.getMessageDomId(msg) + "']);");
1013 }
1014
1015 @Override
1016 public void showExternalResources(final String senderRawAddress) {
1017 mWebView.getSettings().setBlockNetworkImage(false);
1018
1019 final Address sender = getAddress(senderRawAddress);
Tony Mantlerfa674292014-09-11 13:33:56 -07001020 if (sender == null) {
1021 // Don't need to unblock any images
1022 return;
1023 }
Scott Kennedyeb9a4bd2012-11-12 10:33:04 -08001024 final MessageCursor cursor = getMessageCursor();
1025
Tony Mantlerfa674292014-09-11 13:33:56 -07001026 final List<String> messageDomIds = new ArrayList<>();
Scott Kennedyeb9a4bd2012-11-12 10:33:04 -08001027
1028 int pos = -1;
1029 while (cursor.moveToPosition(++pos)) {
1030 final ConversationMessage message = cursor.getMessage();
1031 if (sender.equals(getAddress(message.getFrom()))) {
1032 message.alwaysShowImages = true;
1033
1034 mViewState.setShouldShowImages(message, true);
1035 messageDomIds.add(mTemplates.getMessageDomId(message));
1036 }
1037 }
1038
1039 final String url = String.format(
1040 "javascript:unblockImages(['%s']);", TextUtils.join("','", messageDomIds));
1041 mWebView.loadUrl(url);
Andy Huang3233bff2012-03-20 19:38:45 -07001042 }
Alice Yang1ebc2db2013-03-14 21:21:44 -07001043
1044 @Override
Andy Huang75b52a52013-03-15 15:40:24 -07001045 public boolean supportsMessageTransforms() {
1046 return true;
1047 }
1048
1049 @Override
Alice Yang1ebc2db2013-03-14 21:21:44 -07001050 public String getMessageTransforms(final Message msg) {
1051 final String domId = mTemplates.getMessageDomId(msg);
1052 return (domId == null) ? null : mMessageTransforms.get(domId);
1053 }
1054
James Lemieux8e1ffbf2014-04-22 15:53:31 -07001055 @Override
1056 public boolean isSecure() {
1057 return false;
1058 }
1059
Andy Huang3233bff2012-03-20 19:38:45 -07001060 // END message header callbacks
Andy Huang5ff63742012-03-16 20:30:23 -07001061
Andy Huang46dfba62012-04-19 01:47:32 -07001062 @Override
Andrew Sapperstein2fc67302013-04-29 18:24:56 -07001063 public void showUntransformedConversation() {
1064 super.showUntransformedConversation();
Andrew Sapperstein8c601e42014-07-09 14:37:47 -07001065 final MessageCursor cursor = getMessageCursor();
1066 if (cursor != null) {
1067 renderConversation(cursor);
1068 }
Andrew Sapperstein2fc67302013-04-29 18:24:56 -07001069 }
1070
1071 @Override
Andy Huang46dfba62012-04-19 01:47:32 -07001072 public void onSuperCollapsedClick(SuperCollapsedBlockItem item) {
mindypf4fce122012-09-14 15:55:33 -07001073 MessageCursor cursor = getMessageCursor();
1074 if (cursor == null || !mViewsCreated) {
Andy Huang46dfba62012-04-19 01:47:32 -07001075 return;
1076 }
1077
mindypf4fce122012-09-14 15:55:33 -07001078 mTempBodiesHtml = renderCollapsedHeaders(cursor, item);
Andy Huang46dfba62012-04-19 01:47:32 -07001079 mWebView.loadUrl("javascript:replaceSuperCollapsedBlock(" + item.getStart() + ")");
Jin Cao7ee90b32014-08-14 13:11:25 -07001080 mConversationContainer.focusFirstMessageHeader();
Andy Huang46dfba62012-04-19 01:47:32 -07001081 }
1082
Andy Huang47aa9c92012-07-31 15:37:21 -07001083 private void showNewMessageNotification(NewMessagesInfo info) {
James Lemieux0ec03e82014-09-03 14:01:53 -07001084 mNewMessageBar.show(mNewMessageBarActionListener, info.getNotificationText(), R.string.show,
1085 true /* replaceVisibleToast */, false /* autohide */, null /* ToastBarOperation */);
Andy Huang47aa9c92012-07-31 15:37:21 -07001086 }
1087
1088 private void onNewMessageBarClick() {
James Lemieux0ec03e82014-09-03 14:01:53 -07001089 mNewMessageBar.hide(true, true);
Andy Huang47aa9c92012-07-31 15:37:21 -07001090
mindypf4fce122012-09-14 15:55:33 -07001091 renderConversation(getMessageCursor()); // mCursor is already up-to-date
1092 // per onLoadFinished()
Andy Huang5fbda022012-02-28 18:22:03 -08001093 }
1094
Andrew Sappersteine2b2eb72014-05-13 10:25:34 -07001095 private static OverlayPosition[] parsePositions(final int[] topArray, final int[] bottomArray) {
Andy Huangadbf3e82012-10-13 13:30:19 -07001096 final int len = topArray.length;
1097 final OverlayPosition[] positions = new OverlayPosition[len];
Andy Huangb5078b22012-03-05 19:52:29 -08001098 for (int i = 0; i < len; i++) {
Andrew Sappersteine2b2eb72014-05-13 10:25:34 -07001099 positions[i] = new OverlayPosition(topArray[i], bottomArray[i]);
Andy Huangb5078b22012-03-05 19:52:29 -08001100 }
Andy Huangadbf3e82012-10-13 13:30:19 -07001101 return positions;
Andy Huangb5078b22012-03-05 19:52:29 -08001102 }
1103
Tony Mantlerfa674292014-09-11 13:33:56 -07001104 protected @Nullable Address getAddress(String rawFrom) {
Paul Westbrook0dfae692013-10-02 00:51:29 -07001105 return Utils.getAddress(mAddressCache, rawFrom);
Andy Huang16174812012-08-16 16:40:35 -07001106 }
1107
Andy Huang9d3fd922012-09-26 22:23:58 -07001108 private void ensureContentSizeChangeListener() {
1109 if (mWebViewSizeChangeListener == null) {
Andy Huangc1fb9a92013-02-11 13:09:12 -08001110 mWebViewSizeChangeListener = new ContentSizeChangeListener() {
Andy Huang9d3fd922012-09-26 22:23:58 -07001111 @Override
1112 public void onHeightChange(int h) {
1113 // When WebKit says the DOM height has changed, re-measure
1114 // bodies and re-position their headers.
1115 // This is separate from the typical JavaScript DOM change
1116 // listeners because cases like NARROW_COLUMNS text reflow do not trigger DOM
1117 // events.
1118 mWebView.loadUrl("javascript:measurePositions();");
1119 }
1120 };
1121 }
1122 mWebView.setContentSizeChangeListener(mWebViewSizeChangeListener);
1123 }
1124
Andrew Sapperstein9f957f32013-07-19 15:18:18 -07001125 public static boolean isOverviewMode(Account acct) {
Andy Huangccf67802013-03-15 14:31:57 -07001126 return acct.settings.isOverviewMode();
Andy Huangadbf3e82012-10-13 13:30:19 -07001127 }
1128
1129 private void setupOverviewMode() {
Andy Huang02f9d182012-11-28 22:38:02 -08001130 // for now, overview mode means use the built-in WebView zoom and disable custom scale
1131 // gesture handling
Andy Huangadbf3e82012-10-13 13:30:19 -07001132 final boolean overviewMode = isOverviewMode(mAccount);
1133 final WebSettings settings = mWebView.getSettings();
Andy Huang4dc73232014-02-04 19:58:57 -08001134 final WebSettings.LayoutAlgorithm layout;
Andy Huang06def562012-10-14 00:19:11 -07001135 settings.setUseWideViewPort(overviewMode);
Andy Huang57f354c2013-04-11 17:23:40 -07001136 settings.setSupportZoom(overviewMode);
1137 settings.setBuiltInZoomControls(overviewMode);
Andy Huang4dc73232014-02-04 19:58:57 -08001138 settings.setLoadWithOverviewMode(overviewMode);
Andy Huang57f354c2013-04-11 17:23:40 -07001139 if (overviewMode) {
1140 settings.setDisplayZoomControls(false);
Andy Huang4dc73232014-02-04 19:58:57 -08001141 layout = WebSettings.LayoutAlgorithm.NORMAL;
1142 } else {
1143 layout = WebSettings.LayoutAlgorithm.NARROW_COLUMNS;
Andy Huangadbf3e82012-10-13 13:30:19 -07001144 }
Andy Huang4dc73232014-02-04 19:58:57 -08001145 settings.setLayoutAlgorithm(layout);
Andy Huangadbf3e82012-10-13 13:30:19 -07001146 }
1147
Andy Huang3c6fd442014-03-24 19:56:46 -07001148 @Override
Andrew Sapperstein833123d2014-04-23 18:32:44 -07001149 public ConversationMessage getMessageForClickedUrl(String url) {
Andy Huang3c6fd442014-03-24 19:56:46 -07001150 final String domMessageId = mUrlToMessageIdMap.get(url);
1151 if (domMessageId == null) {
1152 return null;
1153 }
James Lemieux3f6111c2014-09-16 14:34:54 -07001154 final MessageCursor messageCursor = getMessageCursor();
1155 if (messageCursor == null) {
1156 return null;
1157 }
Andy Huang3c6fd442014-03-24 19:56:46 -07001158 final String messageId = mTemplates.getMessageIdForDomId(domMessageId);
James Lemieux3f6111c2014-09-16 14:34:54 -07001159 return messageCursor.getMessageForId(Long.parseLong(messageId));
Andy Huang3c6fd442014-03-24 19:56:46 -07001160 }
1161
Jin Caoc966a8a2014-08-21 17:29:53 -07001162 /**
1163 * Determines if we should intercept the left/right key event generated by the hardware
1164 * keyboard so the framework won't handle directional navigation for us.
1165 */
1166 private boolean shouldInterceptLeftRightEvents(@IdRes int id, boolean isLeft, boolean isRight,
1167 boolean twoPaneLand) {
1168 return twoPaneLand && (id == R.id.conversation_topmost_overlay ||
1169 id == R.id.upper_header ||
1170 id == R.id.super_collapsed_block ||
1171 id == R.id.message_footer ||
1172 (id == R.id.overflow && isRight) ||
1173 (id == R.id.reply_button && isLeft) ||
1174 (id == R.id.forward_button && isRight));
1175 }
1176
1177 /**
1178 * Indicates if the direction with the provided id should navigate away from the conversation
1179 * view. Note that this is only applicable in two-pane landscape mode.
1180 */
1181 private boolean shouldNavigateAway(@IdRes int id, boolean isLeft, boolean twoPaneLand) {
1182 return twoPaneLand && isLeft && (id == R.id.conversation_topmost_overlay ||
1183 id == R.id.upper_header ||
1184 id == R.id.super_collapsed_block ||
1185 id == R.id.message_footer ||
1186 id == R.id.reply_button);
1187 }
1188
Jin Caoa7404582014-08-05 14:03:59 -07001189 @Override
1190 public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
Jin Cao0b693382014-08-11 10:46:12 -07001191 if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
1192 mOriginalKeyedView = view;
1193 }
1194
1195 if (mOriginalKeyedView != null) {
1196 final int id = mOriginalKeyedView.getId();
Jin Cao8da4dc82014-09-10 13:20:57 -07001197 final boolean isRtl = ViewUtils.isViewRtl(mOriginalKeyedView);
Jin Cao0b693382014-08-11 10:46:12 -07001198 final boolean isActionUp = keyEvent.getAction() == KeyEvent.ACTION_UP;
Jin Cao8da4dc82014-09-10 13:20:57 -07001199 final boolean isStart = KeyboardUtils.isKeycodeDirectionStart(keyCode, isRtl);
1200 final boolean isEnd = KeyboardUtils.isKeycodeDirectionEnd(keyCode, isRtl);
Jin Cao0b693382014-08-11 10:46:12 -07001201 final boolean isUp = keyCode == KeyEvent.KEYCODE_DPAD_UP;
1202 final boolean isDown = keyCode == KeyEvent.KEYCODE_DPAD_DOWN;
1203
1204 // First we run the event by the controller
1205 // We manually check if the view+direction combination should shift focus away from the
1206 // conversation view to the thread list in two-pane landscape mode.
1207 final boolean isTwoPaneLand = mNavigationController.isTwoPaneLandscape();
Jin Cao52a40d52014-08-22 17:03:40 -07001208 final boolean navigateAway = shouldNavigateAway(id, isStart, isTwoPaneLand);
Jin Cao0b693382014-08-11 10:46:12 -07001209 if (mNavigationController.onInterceptKeyFromCV(keyCode, keyEvent, navigateAway)) {
1210 return true;
Jin Caoa7404582014-08-05 14:03:59 -07001211 }
Jin Cao0b693382014-08-11 10:46:12 -07001212
1213 // If controller didn't handle the event, check directional interception.
Jin Cao52a40d52014-08-22 17:03:40 -07001214 if ((isStart || isEnd) && shouldInterceptLeftRightEvents(
1215 id, isStart, isEnd, isTwoPaneLand)) {
Jin Cao0b693382014-08-11 10:46:12 -07001216 return true;
1217 } else if (isUp || isDown) {
Jin Cao7ee90b32014-08-14 13:11:25 -07001218 // We don't do anything on up/down for overlay
1219 if (id == R.id.conversation_topmost_overlay) {
1220 return true;
1221 }
1222
Jin Cao0b693382014-08-11 10:46:12 -07001223 // We manually handle up/down navigation through the overlay items because the
1224 // system's default isn't optimal for two-pane landscape since it's not a real list.
Jin Caoea7a5db2014-09-08 15:24:51 -07001225 final View next = mConversationContainer.getNextOverlayView(mOriginalKeyedView,
1226 isDown);
Jin Cao0b693382014-08-11 10:46:12 -07001227 if (next != null) {
Jin Caoea7a5db2014-09-08 15:24:51 -07001228 focusAndScrollToView(next);
1229 } else if (!isActionUp) {
1230 // Scroll in the direction of the arrow if next view isn't found.
1231 final int currentY = mWebView.getScrollY();
1232 if (isUp && currentY > 0) {
1233 mWebView.scrollBy(0,
1234 -Math.min(currentY, DEFAULT_VERTICAL_SCROLL_DISTANCE_PX));
1235 } else if (isDown) {
1236 final int webviewEnd = (int) (mWebView.getContentHeight() *
1237 mWebView.getScale());
1238 final int currentEnd = currentY + mWebView.getHeight();
1239 if (currentEnd < webviewEnd) {
1240 mWebView.scrollBy(0, Math.min(webviewEnd - currentEnd,
1241 DEFAULT_VERTICAL_SCROLL_DISTANCE_PX));
Jin Cao0b693382014-08-11 10:46:12 -07001242 }
1243 }
Jin Cao0b693382014-08-11 10:46:12 -07001244 }
Jin Caoc966a8a2014-08-21 17:29:53 -07001245 return true;
Jin Caoa7404582014-08-05 14:03:59 -07001246 }
Jin Cao0b693382014-08-11 10:46:12 -07001247
1248 // Finally we handle the special keys
1249 if (keyCode == KeyEvent.KEYCODE_BACK && id != R.id.conversation_topmost_overlay) {
1250 if (isActionUp) {
1251 mTopmostOverlay.requestFocus();
1252 }
1253 return true;
1254 } else if (keyCode == KeyEvent.KEYCODE_ENTER &&
1255 id == R.id.conversation_topmost_overlay) {
1256 if (isActionUp) {
Jin Cao7ee90b32014-08-14 13:11:25 -07001257 mWebView.scrollTo(0, 0);
Jin Caoc966a8a2014-08-21 17:29:53 -07001258 mConversationContainer.focusFirstMessageHeader();
Jin Cao0b693382014-08-11 10:46:12 -07001259 }
1260 return true;
1261 }
Jin Caoa7404582014-08-05 14:03:59 -07001262 }
1263 return false;
1264 }
1265
Jin Caoea7a5db2014-09-08 15:24:51 -07001266 private void focusAndScrollToView(View v) {
1267 // Make sure that v is in view
1268 final int[] coords = new int[2];
1269 v.getLocationOnScreen(coords);
1270 final int bottom = coords[1] + v.getHeight();
1271 if (bottom > mMaxScreenHeight) {
1272 mWebView.scrollBy(0, bottom - mMaxScreenHeight);
1273 } else if (coords[1] < mTopOfVisibleScreen) {
1274 mWebView.scrollBy(0, coords[1] - mTopOfVisibleScreen);
1275 }
1276 v.requestFocus();
1277 }
1278
Andrew Sappersteinb1d184d2013-08-09 14:14:31 -07001279 public class ConversationWebViewClient extends AbstractConversationWebViewClient {
Andrew Sapperstein4ddda2f2013-06-10 11:15:38 -07001280 public ConversationWebViewClient(Account account) {
1281 super(account);
Andrew Sapperstein376294b2013-06-06 16:04:26 -07001282 }
1283
Andy Huang17a9cde2012-03-09 18:03:16 -08001284 @Override
James Lemieuxe1302c32014-08-21 15:57:46 -07001285 public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
1286 // try to locate the message associated with the url
1287 final ConversationMessage cm = getMessageForClickedUrl(url);
1288 if (cm != null) {
1289 // try to load the url assuming it is a cid url
1290 final Uri uri = Uri.parse(url);
1291 final WebResourceResponse response = loadCIDUri(uri, cm);
1292 if (response != null) {
1293 return response;
1294 }
1295 }
1296
1297 // otherwise, attempt the default handling
1298 return super.shouldInterceptRequest(view, url);
1299 }
1300
1301 @Override
Andy Huang17a9cde2012-03-09 18:03:16 -08001302 public void onPageFinished(WebView view, String url) {
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001303 // Ignore unsafe calls made after a fragment is detached from an activity.
1304 // This method needs to, for example, get at the loader manager, which needs
1305 // the fragment to be added.
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001306 if (!isAdded() || !mViewsCreated) {
Paul Westbrook006e13c2013-07-24 18:40:20 -07001307 LogUtils.d(LOG_TAG, "ignoring CVF.onPageFinished, url=%s fragment=%s", url,
Andy Huangb95da852012-07-18 14:16:58 -07001308 ConversationViewFragment.this);
1309 return;
1310 }
1311
Paul Westbrook006e13c2013-07-24 18:40:20 -07001312 LogUtils.d(LOG_TAG, "IN CVF.onPageFinished, url=%s fragment=%s wv=%s t=%sms", url,
Andy Huang30bcfe72012-10-18 18:09:03 -07001313 ConversationViewFragment.this, view,
Andy Huang63b3c672012-10-05 19:27:28 -07001314 (SystemClock.uptimeMillis() - mWebViewLoadStartMs));
Andy Huang632721e2012-04-11 16:57:26 -07001315
Andy Huang9d3fd922012-09-26 22:23:58 -07001316 ensureContentSizeChangeListener();
1317
mindyp3bcf1802012-09-09 11:17:00 -07001318 if (!mEnableContentReadySignal) {
Andy Huang7d4746e2012-10-17 17:03:17 -07001319 revealConversation();
mindyp3bcf1802012-09-09 11:17:00 -07001320 }
Andy Huang9d3fd922012-09-26 22:23:58 -07001321
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001322 final Set<String> emailAddresses = Sets.newHashSet();
Andy Huang543e7092013-04-22 11:44:56 -07001323 final List<Address> cacheCopy;
1324 synchronized (mAddressCache) {
1325 cacheCopy = ImmutableList.copyOf(mAddressCache.values());
1326 }
1327 for (Address addr : cacheCopy) {
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001328 emailAddresses.add(addr.getAddress());
Andy Huangb8331b42012-07-16 19:08:53 -07001329 }
Andrew Sapperstein4ddda2f2013-06-10 11:15:38 -07001330 final ContactLoaderCallbacks callbacks = getContactInfoSource();
1331 callbacks.setSenders(emailAddresses);
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001332 getLoaderManager().restartLoader(CONTACT_LOADER, Bundle.EMPTY, callbacks);
Andy Huang17a9cde2012-03-09 18:03:16 -08001333 }
1334
Andy Huangaf5d4e02012-03-19 19:02:12 -07001335 @Override
1336 public boolean shouldOverrideUrlLoading(WebView view, String url) {
Paul Westbrook542fec92012-09-18 14:47:51 -07001337 return mViewsCreated && super.shouldOverrideUrlLoading(view, url);
Andy Huangaf5d4e02012-03-19 19:02:12 -07001338 }
Andy Huang17a9cde2012-03-09 18:03:16 -08001339 }
1340
Andy Huangf70fc402012-02-17 15:37:42 -08001341 /**
1342 * NOTE: all public methods must be listed in the proguard flags so that they can be accessed
1343 * via reflection and not stripped.
1344 *
1345 */
1346 private class MailJsBridge {
Mindy Pereira974c9662012-09-14 10:02:08 -07001347 @JavascriptInterface
Andrew Sappersteine2b2eb72014-05-13 10:25:34 -07001348 public void onWebContentGeometryChange(final int[] overlayTopStrs,
1349 final int[] overlayBottomStrs) {
Andrew Sapperstein8ec43e82013-12-17 18:27:55 -08001350 try {
1351 getHandler().post(new FragmentRunnable("onWebContentGeometryChange",
1352 ConversationViewFragment.this) {
1353 @Override
1354 public void go() {
Andy Huang46dfba62012-04-19 01:47:32 -07001355 if (!mViewsCreated) {
mindyp1b3cc472012-09-27 11:32:59 -07001356 LogUtils.d(LOG_TAG, "ignoring webContentGeometryChange because views"
1357 + " are gone, %s", ConversationViewFragment.this);
Andy Huang46dfba62012-04-19 01:47:32 -07001358 return;
1359 }
Andy Huangadbf3e82012-10-13 13:30:19 -07001360 mConversationContainer.onGeometryChange(
1361 parsePositions(overlayTopStrs, overlayBottomStrs));
mindyp1b3cc472012-09-27 11:32:59 -07001362 if (mDiff != 0) {
1363 // SCROLL!
1364 int scale = (int) (mWebView.getScale() / mWebView.getInitialScale());
1365 if (scale > 1) {
1366 mWebView.scrollBy(0, (mDiff * (scale - 1)));
1367 }
1368 mDiff = 0;
1369 }
Andy Huang46dfba62012-04-19 01:47:32 -07001370 }
Andrew Sapperstein8ec43e82013-12-17 18:27:55 -08001371 });
1372 } catch (Throwable t) {
1373 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onWebContentGeometryChange");
1374 }
Andy Huang46dfba62012-04-19 01:47:32 -07001375 }
1376
Mindy Pereira974c9662012-09-14 10:02:08 -07001377 @JavascriptInterface
Andy Huang46dfba62012-04-19 01:47:32 -07001378 public String getTempMessageBodies() {
1379 try {
1380 if (!mViewsCreated) {
1381 return "";
Andy Huangf70fc402012-02-17 15:37:42 -08001382 }
Andy Huang46dfba62012-04-19 01:47:32 -07001383
1384 final String s = mTempBodiesHtml;
1385 mTempBodiesHtml = null;
1386 return s;
1387 } catch (Throwable t) {
1388 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getTempMessageBodies");
1389 return "";
1390 }
Andy Huangf70fc402012-02-17 15:37:42 -08001391 }
1392
Andy Huang014ea4c2012-09-25 14:50:54 -07001393 @JavascriptInterface
1394 public String getMessageBody(String domId) {
1395 try {
1396 final MessageCursor cursor = getMessageCursor();
1397 if (!mViewsCreated || cursor == null) {
1398 return "";
1399 }
1400
1401 int pos = -1;
1402 while (cursor.moveToPosition(++pos)) {
1403 final ConversationMessage msg = cursor.getMessage();
1404 if (TextUtils.equals(domId, mTemplates.getMessageDomId(msg))) {
Andy Huang986776b2014-02-19 18:29:43 -08001405 return HtmlConversationTemplates.wrapMessageBody(msg.getBodyAsHtml());
Andy Huang014ea4c2012-09-25 14:50:54 -07001406 }
1407 }
1408
1409 return "";
1410
1411 } catch (Throwable t) {
1412 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getMessageBody");
1413 return "";
1414 }
1415 }
1416
Mindy Pereira974c9662012-09-14 10:02:08 -07001417 @JavascriptInterface
Andy Huang543e7092013-04-22 11:44:56 -07001418 public String getMessageSender(String domId) {
1419 try {
1420 final MessageCursor cursor = getMessageCursor();
1421 if (!mViewsCreated || cursor == null) {
1422 return "";
1423 }
1424
1425 int pos = -1;
1426 while (cursor.moveToPosition(++pos)) {
1427 final ConversationMessage msg = cursor.getMessage();
1428 if (TextUtils.equals(domId, mTemplates.getMessageDomId(msg))) {
Tony Mantlerfa674292014-09-11 13:33:56 -07001429 final Address address = getAddress(msg.getFrom());
1430 if (address != null) {
1431 return address.getAddress();
1432 } else {
1433 // Fall through to return an empty string
1434 break;
1435 }
Andy Huang543e7092013-04-22 11:44:56 -07001436 }
1437 }
1438
1439 return "";
1440
1441 } catch (Throwable t) {
1442 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getMessageSender");
1443 return "";
1444 }
1445 }
1446
Andy Huang543e7092013-04-22 11:44:56 -07001447 @JavascriptInterface
mindyp3bcf1802012-09-09 11:17:00 -07001448 public void onContentReady() {
Andrew Sapperstein8ec43e82013-12-17 18:27:55 -08001449 try {
1450 getHandler().post(new FragmentRunnable("onContentReady",
1451 ConversationViewFragment.this) {
1452 @Override
1453 public void go() {
1454 try {
1455 if (mWebViewLoadStartMs != 0) {
1456 LogUtils.i(LOG_TAG, "IN CVF.onContentReady, f=%s vis=%s t=%sms",
1457 ConversationViewFragment.this,
1458 isUserVisible(),
1459 (SystemClock.uptimeMillis() - mWebViewLoadStartMs));
1460 }
1461 revealConversation();
1462 } catch (Throwable t) {
1463 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onContentReady");
1464 // Still try to show the conversation.
1465 revealConversation();
Andy Huang63b3c672012-10-05 19:27:28 -07001466 }
mindyp3bcf1802012-09-09 11:17:00 -07001467 }
Andrew Sapperstein8ec43e82013-12-17 18:27:55 -08001468 });
1469 } catch (Throwable t) {
1470 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onContentReady");
1471 }
mindyp3bcf1802012-09-09 11:17:00 -07001472 }
Andy Huange964eee2012-10-02 19:24:58 -07001473
Andy Huange964eee2012-10-02 19:24:58 -07001474 @JavascriptInterface
1475 public float getScrollYPercent() {
1476 try {
1477 return mWebViewYPercent;
1478 } catch (Throwable t) {
1479 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getScrollYPercent");
1480 return 0f;
1481 }
1482 }
Andy Huang05c70c82013-03-14 15:15:50 -07001483
Andy Huang05c70c82013-03-14 15:15:50 -07001484 @JavascriptInterface
1485 public void onMessageTransform(String messageDomId, String transformText) {
Andrew Sappersteinae92e152013-05-03 13:55:18 -07001486 try {
1487 LogUtils.i(LOG_TAG, "TRANSFORM: (%s) %s", messageDomId, transformText);
1488 mMessageTransforms.put(messageDomId, transformText);
1489 onConversationTransformed();
1490 } catch (Throwable t) {
1491 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onMessageTransform");
Andrew Sapperstein8ec43e82013-12-17 18:27:55 -08001492 }
1493 }
1494
1495 @JavascriptInterface
1496 public void onInlineAttachmentsParsed(final String[] urls, final String[] messageIds) {
1497 try {
1498 getHandler().post(new FragmentRunnable("onInlineAttachmentsParsed",
1499 ConversationViewFragment.this) {
1500 @Override
1501 public void go() {
1502 try {
1503 for (int i = 0, size = urls.length; i < size; i++) {
1504 mUrlToMessageIdMap.put(urls[i], messageIds[i]);
1505 }
1506 } catch (ArrayIndexOutOfBoundsException e) {
1507 LogUtils.e(LOG_TAG, e,
1508 "Number of urls does not match number of message ids - %s:%s",
1509 urls.length, messageIds.length);
1510 }
1511 }
1512 });
1513 } catch (Throwable t) {
1514 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onInlineAttachmentsParsed");
Andrew Sappersteinae92e152013-05-03 13:55:18 -07001515 }
Andy Huang05c70c82013-03-14 15:15:50 -07001516 }
Andy Huangf70fc402012-02-17 15:37:42 -08001517 }
1518
Andy Huang47aa9c92012-07-31 15:37:21 -07001519 private class NewMessagesInfo {
1520 int count;
Andy Huang06c03622012-10-22 18:59:45 -07001521 int countFromSelf;
Andy Huang47aa9c92012-07-31 15:37:21 -07001522
1523 /**
1524 * Return the display text for the new message notification overlay. It will be formatted
1525 * appropriately for a single new message vs. multiple new messages.
1526 *
1527 * @return display text
1528 */
1529 public String getNotificationText() {
James Lemieux0ec03e82014-09-03 14:01:53 -07001530 return getResources().getQuantityString(R.plurals.new_incoming_messages, count, count);
Andy Huang47aa9c92012-07-31 15:37:21 -07001531 }
1532 }
1533
mindypf4fce122012-09-14 15:55:33 -07001534 @Override
Paul Westbrookc42ad5e2013-05-09 16:52:15 -07001535 public void onMessageCursorLoadFinished(Loader<ObjectCursor<ConversationMessage>> loader,
1536 MessageCursor newCursor, MessageCursor oldCursor) {
mindypf4fce122012-09-14 15:55:33 -07001537 /*
1538 * what kind of changes affect the MessageCursor? 1. new message(s) 2.
1539 * read/unread state change 3. deleted message, either regular or draft
1540 * 4. updated message, either from self or from others, updated in
1541 * content or state or sender 5. star/unstar of message (technically
1542 * similar to #1) 6. other label change Use MessageCursor.hashCode() to
1543 * sort out interesting vs. no-op cursor updates.
1544 */
Andy Huangb8331b42012-07-16 19:08:53 -07001545
Andy Huang233d4352012-10-18 14:00:24 -07001546 if (oldCursor != null && !oldCursor.isClosed()) {
Andy Huang014ea4c2012-09-25 14:50:54 -07001547 final NewMessagesInfo info = getNewIncomingMessagesInfo(newCursor);
Andy Huangb8331b42012-07-16 19:08:53 -07001548
Andy Huang014ea4c2012-09-25 14:50:54 -07001549 if (info.count > 0) {
1550 // don't immediately render new incoming messages from other
1551 // senders
1552 // (to avoid a new message from losing the user's focus)
1553 LogUtils.i(LOG_TAG, "CONV RENDER: conversation updated"
Andy Huang9d3fd922012-09-26 22:23:58 -07001554 + ", holding cursor for new incoming message (%s)", this);
Andy Huang014ea4c2012-09-25 14:50:54 -07001555 showNewMessageNotification(info);
1556 return;
1557 }
1558
Andy Huang06c03622012-10-22 18:59:45 -07001559 final int oldState = oldCursor.getStateHashCode();
1560 final boolean changed = newCursor.getStateHashCode() != oldState;
Andy Huang233d4352012-10-18 14:00:24 -07001561
Andy Huang014ea4c2012-09-25 14:50:54 -07001562 if (!changed) {
1563 final boolean processedInPlace = processInPlaceUpdates(newCursor, oldCursor);
1564 if (processedInPlace) {
Andy Huang9d3fd922012-09-26 22:23:58 -07001565 LogUtils.i(LOG_TAG, "CONV RENDER: processed update(s) in place (%s)", this);
Andy Huang1ee96b22012-08-24 20:19:53 -07001566 } else {
mindypf4fce122012-09-14 15:55:33 -07001567 LogUtils.i(LOG_TAG, "CONV RENDER: uninteresting update"
Andy Huang9d3fd922012-09-26 22:23:58 -07001568 + ", ignoring this conversation update (%s)", this);
Andy Huang1ee96b22012-08-24 20:19:53 -07001569 }
Andy Huangb8331b42012-07-16 19:08:53 -07001570 return;
Andy Huang06c03622012-10-22 18:59:45 -07001571 } else if (info.countFromSelf == 1) {
1572 // Special-case the very common case of a new cursor that is the same as the old
1573 // one, except that there is a new message from yourself. This happens upon send.
1574 final boolean sameExceptNewLast = newCursor.getStateHashCode(1) == oldState;
1575 if (sameExceptNewLast) {
1576 LogUtils.i(LOG_TAG, "CONV RENDER: update is a single new message from self"
1577 + " (%s)", this);
1578 newCursor.moveToLast();
1579 processNewOutgoingMessage(newCursor.getMessage());
1580 return;
1581 }
Andy Huangb8331b42012-07-16 19:08:53 -07001582 }
Andy Huang6766b6e2012-09-28 12:43:52 -07001583 // cursors are different, and not due to an incoming message. fall
1584 // through and render.
1585 LogUtils.i(LOG_TAG, "CONV RENDER: conversation updated"
1586 + ", but not due to incoming message. rendering. (%s)", this);
Andy Huang06c03622012-10-22 18:59:45 -07001587
1588 if (DEBUG_DUMP_CURSOR_CONTENTS) {
1589 LogUtils.i(LOG_TAG, "old cursor: %s", oldCursor.getDebugDump());
1590 LogUtils.i(LOG_TAG, "new cursor: %s", newCursor.getDebugDump());
1591 }
Andy Huang6766b6e2012-09-28 12:43:52 -07001592 } else {
1593 LogUtils.i(LOG_TAG, "CONV RENDER: initial render. (%s)", this);
Andy Huang243c2362013-03-01 17:50:35 -08001594 timerMark("message cursor load finished");
Andy Huangb8331b42012-07-16 19:08:53 -07001595 }
1596
Andrew Sapperstein606dbd72013-07-30 19:14:23 -07001597 renderContent(newCursor);
1598 }
1599
1600 protected void renderContent(MessageCursor messageCursor) {
Mark Wei4071c2f2012-09-26 14:38:38 -07001601 // if layout hasn't happened, delay render
1602 // This is needed in addition to the showConversation() delay to speed
1603 // up rotation and restoration.
1604 if (mConversationContainer.getWidth() == 0) {
1605 mNeedRender = true;
1606 mConversationContainer.addOnLayoutChangeListener(this);
1607 } else {
Andrew Sapperstein606dbd72013-07-30 19:14:23 -07001608 renderConversation(messageCursor);
Mark Wei4071c2f2012-09-26 14:38:38 -07001609 }
Andy Huangb8331b42012-07-16 19:08:53 -07001610 }
1611
mindypf4fce122012-09-14 15:55:33 -07001612 private NewMessagesInfo getNewIncomingMessagesInfo(MessageCursor newCursor) {
1613 final NewMessagesInfo info = new NewMessagesInfo();
Andy Huangb8331b42012-07-16 19:08:53 -07001614
mindypf4fce122012-09-14 15:55:33 -07001615 int pos = -1;
1616 while (newCursor.moveToPosition(++pos)) {
1617 final Message m = newCursor.getMessage();
1618 if (!mViewState.contains(m)) {
1619 LogUtils.i(LOG_TAG, "conversation diff: found new msg: %s", m.uri);
Andy Huangb8331b42012-07-16 19:08:53 -07001620
Scott Kennedy8960f0a2012-11-07 15:35:50 -08001621 final Address from = getAddress(m.getFrom());
mindypf4fce122012-09-14 15:55:33 -07001622 // distinguish ours from theirs
1623 // new messages from the account owner should not trigger a
1624 // notification
Paul Westbrook1353da42014-08-15 12:23:01 -07001625 if (from == null || mAccount.ownsFromAddress(from.getAddress())) {
mindypf4fce122012-09-14 15:55:33 -07001626 LogUtils.i(LOG_TAG, "found message from self: %s", m.uri);
Andy Huang06c03622012-10-22 18:59:45 -07001627 info.countFromSelf++;
mindypf4fce122012-09-14 15:55:33 -07001628 continue;
1629 }
Andy Huangb8331b42012-07-16 19:08:53 -07001630
mindypf4fce122012-09-14 15:55:33 -07001631 info.count++;
Andy Huangb8331b42012-07-16 19:08:53 -07001632 }
Andy Huangb8331b42012-07-16 19:08:53 -07001633 }
mindypf4fce122012-09-14 15:55:33 -07001634 return info;
Andy Huangb8331b42012-07-16 19:08:53 -07001635 }
1636
Andy Huang014ea4c2012-09-25 14:50:54 -07001637 private boolean processInPlaceUpdates(MessageCursor newCursor, MessageCursor oldCursor) {
1638 final Set<String> idsOfChangedBodies = Sets.newHashSet();
Andy Huang6b3d0d92012-10-30 15:46:48 -07001639 final List<Integer> changedOverlayPositions = Lists.newArrayList();
1640
Andy Huang014ea4c2012-09-25 14:50:54 -07001641 boolean changed = false;
1642
1643 int pos = 0;
1644 while (true) {
1645 if (!newCursor.moveToPosition(pos) || !oldCursor.moveToPosition(pos)) {
1646 break;
1647 }
1648
1649 final ConversationMessage newMsg = newCursor.getMessage();
1650 final ConversationMessage oldMsg = oldCursor.getMessage();
1651
Jin Cao2ef5f082014-03-31 14:52:38 -07001652 // We are going to update the data in the adapter whenever any input fields change.
1653 // This ensures that the Message object that ComposeActivity uses will be correctly
1654 // aligned with the most up-to-date data.
Tony Mantler9ac224f2014-09-24 11:00:28 -07001655 if (!newMsg.isEqual(oldMsg)) {
Andy Huang6b3d0d92012-10-30 15:46:48 -07001656 mAdapter.updateItemsForMessage(newMsg, changedOverlayPositions);
Jin Cao6a2df252014-05-29 15:12:33 -07001657 LogUtils.i(LOG_TAG, "msg #%d (%d): detected field(s) change. sendingState=%s",
1658 pos, newMsg.id, newMsg.sendingState);
Andy Huang014ea4c2012-09-25 14:50:54 -07001659 }
1660
1661 // update changed message bodies in-place
1662 if (!TextUtils.equals(newMsg.bodyHtml, oldMsg.bodyHtml) ||
1663 !TextUtils.equals(newMsg.bodyText, oldMsg.bodyText)) {
1664 // maybe just set a flag to notify JS to re-request changed bodies
1665 idsOfChangedBodies.add('"' + mTemplates.getMessageDomId(newMsg) + '"');
1666 LogUtils.i(LOG_TAG, "msg #%d (%d): detected body change", pos, newMsg.id);
1667 }
1668
1669 pos++;
1670 }
1671
Andy Huang6b3d0d92012-10-30 15:46:48 -07001672
1673 if (!changedOverlayPositions.isEmpty()) {
Andy Huang06c03622012-10-22 18:59:45 -07001674 // notify once after the entire adapter is updated
Andy Huang6b3d0d92012-10-30 15:46:48 -07001675 mConversationContainer.onOverlayModelUpdate(changedOverlayPositions);
1676 changed = true;
Andy Huang06c03622012-10-22 18:59:45 -07001677 }
1678
Andrew Sapperstein735a22a2014-07-11 03:57:42 -07001679 final ConversationFooterItem footerItem = mAdapter.getFooterItem();
1680 if (footerItem != null) {
1681 footerItem.invalidateMeasurement();
1682 }
Andy Huang014ea4c2012-09-25 14:50:54 -07001683 if (!idsOfChangedBodies.isEmpty()) {
1684 mWebView.loadUrl(String.format("javascript:replaceMessageBodies([%s]);",
1685 TextUtils.join(",", idsOfChangedBodies)));
1686 changed = true;
1687 }
1688
1689 return changed;
1690 }
1691
Andy Huang06c03622012-10-22 18:59:45 -07001692 private void processNewOutgoingMessage(ConversationMessage msg) {
Andrew Sappersteine2a30e12014-07-02 22:36:56 -07001693 // Temporarily remove the ConversationFooterItem and its view.
1694 // It will get re-added right after the new message is added.
1695 final ConversationFooterItem footerItem = mAdapter.removeFooterItem();
Andrew Sapperstein9fe12e92014-09-05 12:08:06 -07001696 // if no footer, just skip the work for it. The rest should be fine to do.
Andrew Sapperstein38689a82014-09-05 10:37:07 -07001697 if (footerItem != null) {
Andrew Sapperstein38689a82014-09-05 10:37:07 -07001698 mConversationContainer.removeViewAtAdapterIndex(footerItem.getPosition());
1699 }
Andrew Sapperstein9fe12e92014-09-05 12:08:06 -07001700
Andy Huang06c03622012-10-22 18:59:45 -07001701 mTemplates.reset();
1702 // this method will add some items to mAdapter, but we deliberately want to avoid notifying
1703 // adapter listeners (i.e. ConversationContainer) until onWebContentGeometryChange is next
1704 // called, to prevent N+1 headers rendering with N message bodies.
Andrew Sappersteine2a30e12014-07-02 22:36:56 -07001705 renderMessage(msg, true /* expanded */, msg.alwaysShowImages);
Andy Huang06c03622012-10-22 18:59:45 -07001706 mTempBodiesHtml = mTemplates.emit();
1707
Andrew Sappersteine2a30e12014-07-02 22:36:56 -07001708 if (footerItem != null) {
1709 footerItem.setLastMessageHeaderItem(getLastMessageHeaderItem());
Andrew Sapperstein735a22a2014-07-11 03:57:42 -07001710 footerItem.invalidateMeasurement();
Andrew Sappersteine2a30e12014-07-02 22:36:56 -07001711 mAdapter.addItem(footerItem);
1712 }
1713
Andy Huang06c03622012-10-22 18:59:45 -07001714 mViewState.setExpansionState(msg, ExpansionState.EXPANDED);
1715 // FIXME: should the provider set this as initial state?
1716 mViewState.setReadState(msg, false /* read */);
1717
Andy Huang91d782a2012-10-25 12:37:29 -07001718 // From now until the updated spacer geometry is returned, the adapter items are mismatched
1719 // with the existing spacers. Do not let them layout.
1720 mConversationContainer.invalidateSpacerGeometry();
1721
Andy Huang06c03622012-10-22 18:59:45 -07001722 mWebView.loadUrl("javascript:appendMessageHtml();");
1723 }
1724
Andrew Sappersteinf59d01c2014-02-20 10:27:06 -08001725 private static class SetCookieTask extends AsyncTask<Void, Void, Void> {
1726 private final Context mContext;
1727 private final String mUri;
1728 private final Uri mAccountCookieQueryUri;
1729 private final ContentResolver mResolver;
Paul Westbrookcebcc642012-08-08 10:06:04 -07001730
Andrew Sappersteinf59d01c2014-02-20 10:27:06 -08001731 /* package */ SetCookieTask(Context context, String baseUri, Uri accountCookieQueryUri) {
1732 mContext = context;
1733 mUri = baseUri;
Paul Westbrookb8361c92012-09-27 10:57:14 -07001734 mAccountCookieQueryUri = accountCookieQueryUri;
1735 mResolver = context.getContentResolver();
Paul Westbrookcebcc642012-08-08 10:06:04 -07001736 }
1737
1738 @Override
1739 public Void doInBackground(Void... args) {
Andrew Sappersteinf59d01c2014-02-20 10:27:06 -08001740 // First query for the cookie string from the UI provider
Paul Westbrookb8361c92012-09-27 10:57:14 -07001741 final Cursor cookieCursor = mResolver.query(mAccountCookieQueryUri,
1742 UIProvider.ACCOUNT_COOKIE_PROJECTION, null, null, null);
1743 if (cookieCursor == null) {
1744 return null;
1745 }
1746
1747 try {
1748 if (cookieCursor.moveToFirst()) {
1749 final String cookie = cookieCursor.getString(
1750 cookieCursor.getColumnIndex(UIProvider.AccountCookieColumns.COOKIE));
1751
1752 if (cookie != null) {
1753 final CookieSyncManager csm =
Andrew Sappersteinf59d01c2014-02-20 10:27:06 -08001754 CookieSyncManager.createInstance(mContext);
Paul Westbrookb8361c92012-09-27 10:57:14 -07001755 CookieManager.getInstance().setCookie(mUri, cookie);
1756 csm.sync();
1757 }
1758 }
1759
1760 } finally {
1761 cookieCursor.close();
1762 }
1763
1764
Paul Westbrookcebcc642012-08-08 10:06:04 -07001765 return null;
1766 }
1767 }
mindyp36280f32012-09-09 16:11:23 -07001768
mindyp26d4d2d2012-09-18 17:30:32 -07001769 @Override
mindyp36280f32012-09-09 16:11:23 -07001770 public void onConversationUpdated(Conversation conv) {
1771 final ConversationViewHeader headerView = (ConversationViewHeader) mConversationContainer
1772 .findViewById(R.id.conversation_header);
mindypb2b98ba2012-09-24 14:13:58 -07001773 mConversation = conv;
mindyp9e0b2362012-09-09 16:31:21 -07001774 if (headerView != null) {
1775 headerView.onConversationUpdated(conv);
1776 }
mindyp36280f32012-09-09 16:11:23 -07001777 }
Mark Wei4071c2f2012-09-26 14:38:38 -07001778
1779 @Override
1780 public void onLayoutChange(View v, int left, int top, int right,
1781 int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
1782 boolean sizeChanged = mNeedRender
1783 && mConversationContainer.getWidth() != 0;
1784 if (sizeChanged) {
1785 mNeedRender = false;
1786 mConversationContainer.removeOnLayoutChangeListener(this);
1787 renderConversation(getMessageCursor());
1788 }
1789 }
mindyp1b3cc472012-09-27 11:32:59 -07001790
1791 @Override
James Lemieux7cad2802014-01-09 15:00:53 -08001792 public void setMessageDetailsExpanded(MessageHeaderItem i, boolean expanded, int heightBefore) {
mindyp1b3cc472012-09-27 11:32:59 -07001793 mDiff = (expanded ? 1 : -1) * Math.abs(i.getHeight() - heightBefore);
1794 }
Andy Huang02f9d182012-11-28 22:38:02 -08001795
James Lemieux7cad2802014-01-09 15:00:53 -08001796 /**
1797 * @return {@code true} because either the Print or Print All menu item is shown in GMail
1798 */
1799 @Override
1800 protected boolean shouldShowPrintInOverflow() {
1801 return true;
1802 }
1803
1804 @Override
Andrew Sapperstein5c1692a2013-09-16 11:56:13 -07001805 protected void printConversation() {
Andrew Sapperstein234d3532013-10-29 14:54:04 -07001806 PrintUtils.printConversation(mActivity.getActivityContext(), getMessageCursor(),
1807 mAddressCache, mConversation.getBaseUri(mBaseUri), true /* useJavascript */);
Andrew Sapperstein5c1692a2013-09-16 11:56:13 -07001808 }
Mindy Pereira9b875682012-02-15 18:10:54 -08001809}