blob: 6fb683f948b1d1078baabe23244a37c28e4609ed [file] [log] [blame]
Mindy Pereira9b875682012-02-15 18:10:54 -08001/*
2 * Copyright (C) 2012 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.mail.ui;
19
mindypf4fce122012-09-14 15:55:33 -070020
Paul Westbrookb8361c92012-09-27 10:57:14 -070021import android.content.ContentResolver;
Mindy Pereira9b875682012-02-15 18:10:54 -080022import android.content.Context;
Mindy Pereira8e915722012-02-16 14:42:56 -080023import android.content.Loader;
mindypad0c30d2012-09-25 12:09:13 -070024import android.content.res.Resources;
Mindy Pereira9b875682012-02-15 18:10:54 -080025import android.database.Cursor;
Andy Huang9d3fd922012-09-26 22:23:58 -070026import android.database.DataSetObserver;
Paul Westbrookb8361c92012-09-27 10:57:14 -070027import android.net.Uri;
Paul Westbrookcebcc642012-08-08 10:06:04 -070028import android.os.AsyncTask;
Mindy Pereira9b875682012-02-15 18:10:54 -080029import android.os.Bundle;
mindyp3bcf1802012-09-09 11:17:00 -070030import android.os.SystemClock;
Andy Huang47aa9c92012-07-31 15:37:21 -070031import android.text.TextUtils;
Mindy Pereira9b875682012-02-15 18:10:54 -080032import android.view.LayoutInflater;
33import android.view.View;
Mark Wei4071c2f2012-09-26 14:38:38 -070034import android.view.View.OnLayoutChangeListener;
Mindy Pereira9b875682012-02-15 18:10:54 -080035import android.view.ViewGroup;
Andy Huangf70fc402012-02-17 15:37:42 -080036import android.webkit.ConsoleMessage;
Paul Westbrookcebcc642012-08-08 10:06:04 -070037import android.webkit.CookieManager;
38import android.webkit.CookieSyncManager;
Mindy Pereira974c9662012-09-14 10:02:08 -070039import android.webkit.JavascriptInterface;
Andy Huangf70fc402012-02-17 15:37:42 -080040import android.webkit.WebChromeClient;
41import android.webkit.WebSettings;
Andy Huang17a9cde2012-03-09 18:03:16 -080042import android.webkit.WebView;
43import android.webkit.WebViewClient;
Andy Huang47aa9c92012-07-31 15:37:21 -070044import android.widget.TextView;
Mindy Pereira9b875682012-02-15 18:10:54 -080045
Andy Huang59e0b182012-08-14 14:32:23 -070046import com.android.mail.FormattedDateBuilder;
Mindy Pereira9b875682012-02-15 18:10:54 -080047import com.android.mail.R;
Andy Huang5ff63742012-03-16 20:30:23 -070048import com.android.mail.browse.ConversationContainer;
Andy Huangadbf3e82012-10-13 13:30:19 -070049import com.android.mail.browse.ConversationContainer.OverlayPosition;
Andy Huang46dfba62012-04-19 01:47:32 -070050import com.android.mail.browse.ConversationOverlayItem;
Andy Huang7bdc3752012-03-25 17:18:19 -070051import com.android.mail.browse.ConversationViewAdapter;
Andy Huang46dfba62012-04-19 01:47:32 -070052import com.android.mail.browse.ConversationViewAdapter.MessageFooterItem;
Andy Huang7bdc3752012-03-25 17:18:19 -070053import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem;
Andy Huang46dfba62012-04-19 01:47:32 -070054import com.android.mail.browse.ConversationViewAdapter.SuperCollapsedBlockItem;
Andy Huang5ff63742012-03-16 20:30:23 -070055import com.android.mail.browse.ConversationViewHeader;
56import com.android.mail.browse.ConversationWebView;
mindypdde3f9f2012-09-10 17:35:35 -070057import com.android.mail.browse.ConversationWebView.ContentSizeChangeListener;
Andy Huang7bdc3752012-03-25 17:18:19 -070058import com.android.mail.browse.MessageCursor;
Andy Huangcd5c5ee2012-08-12 19:03:51 -070059import com.android.mail.browse.MessageCursor.ConversationController;
Andy Huang28b7aee2012-08-20 20:27:32 -070060import com.android.mail.browse.MessageCursor.ConversationMessage;
Andy Huang59e0b182012-08-14 14:32:23 -070061import com.android.mail.browse.MessageHeaderView;
Andy Huang3233bff2012-03-20 19:38:45 -070062import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks;
Andy Huangadbf3e82012-10-13 13:30:19 -070063import com.android.mail.browse.ScrollIndicatorsView;
Andy Huang46dfba62012-04-19 01:47:32 -070064import com.android.mail.browse.SuperCollapsedBlock;
Andy Huang0b7ed6f2012-07-25 19:23:26 -070065import com.android.mail.browse.WebViewContextMenu;
Mindy Pereira9b875682012-02-15 18:10:54 -080066import com.android.mail.providers.Account;
Andy Huang65fe28f2012-04-06 18:08:53 -070067import com.android.mail.providers.Address;
Mindy Pereira9b875682012-02-15 18:10:54 -080068import com.android.mail.providers.Conversation;
Andy Huangf70fc402012-02-17 15:37:42 -080069import com.android.mail.providers.Message;
Paul Westbrookb8361c92012-09-27 10:57:14 -070070import com.android.mail.providers.UIProvider;
Andy Huangcd5c5ee2012-08-12 19:03:51 -070071import com.android.mail.ui.ConversationViewState.ExpansionState;
Paul Westbrookb334c902012-06-25 11:42:46 -070072import com.android.mail.utils.LogTag;
Mindy Pereira9b875682012-02-15 18:10:54 -080073import com.android.mail.utils.LogUtils;
Andy Huang2e9acfe2012-03-15 22:39:36 -070074import com.android.mail.utils.Utils;
Andy Huang46dfba62012-04-19 01:47:32 -070075import com.google.common.collect.Lists;
Andy Huangb8331b42012-07-16 19:08:53 -070076import com.google.common.collect.Sets;
Andy Huang65fe28f2012-04-06 18:08:53 -070077
Andy Huang46dfba62012-04-19 01:47:32 -070078import java.util.List;
Andy Huangb8331b42012-07-16 19:08:53 -070079import java.util.Set;
Mindy Pereira9b875682012-02-15 18:10:54 -080080
Andy Huangf70fc402012-02-17 15:37:42 -080081
Mindy Pereira9b875682012-02-15 18:10:54 -080082/**
83 * The conversation view UI component.
84 */
mindypf4fce122012-09-14 15:55:33 -070085public final class ConversationViewFragment extends AbstractConversationViewFragment implements
Andy Huangcd5c5ee2012-08-12 19:03:51 -070086 SuperCollapsedBlock.OnClickListener,
Mark Wei4071c2f2012-09-26 14:38:38 -070087 OnLayoutChangeListener {
Mindy Pereira8e915722012-02-16 14:42:56 -080088
Paul Westbrookb334c902012-06-25 11:42:46 -070089 private static final String LOG_TAG = LogTag.getLogTag();
Andy Huang632721e2012-04-11 16:57:26 -070090 public static final String LAYOUT_TAG = "ConvLayout";
Mindy Pereira9b875682012-02-15 18:10:54 -080091
Andy Huang9d3fd922012-09-26 22:23:58 -070092 /**
mindyp1b3cc472012-09-27 11:32:59 -070093 * Difference in the height of the message header whose details have been expanded/collapsed
94 */
95 private int mDiff = 0;
96
97 /**
Andy Huang9d3fd922012-09-26 22:23:58 -070098 * Default value for {@link #mLoadWaitReason}. Conversation load will happen immediately.
99 */
100 private final int LOAD_NOW = 0;
101 /**
102 * Value for {@link #mLoadWaitReason} that means we are offscreen and waiting for the visible
103 * conversation to finish loading before beginning our load.
104 * <p>
105 * When this value is set, the fragment should register with {@link ConversationListCallbacks}
106 * to know when the visible conversation is loaded. When it is unset, it should unregister.
107 */
108 private final int LOAD_WAIT_FOR_INITIAL_CONVERSATION = 1;
109 /**
110 * Value for {@link #mLoadWaitReason} used when a conversation is too heavyweight to load at
111 * all when not visible (e.g. requires network fetch, or too complex). Conversation load will
112 * wait until this fragment is visible.
113 */
114 private final int LOAD_WAIT_UNTIL_VISIBLE = 2;
mindyp3bcf1802012-09-09 11:17:00 -0700115
Andy Huangf70fc402012-02-17 15:37:42 -0800116 private ConversationContainer mConversationContainer;
Mindy Pereira9b875682012-02-15 18:10:54 -0800117
Andy Huangf70fc402012-02-17 15:37:42 -0800118 private ConversationWebView mWebView;
Mindy Pereira9b875682012-02-15 18:10:54 -0800119
Mark Wei56d83852012-09-19 14:28:50 -0700120 private ScrollIndicatorsView mScrollIndicators;
121
Andy Huang47aa9c92012-07-31 15:37:21 -0700122 private View mNewMessageBar;
123
Andy Huangf70fc402012-02-17 15:37:42 -0800124 private HtmlConversationTemplates mTemplates;
125
Andy Huangf70fc402012-02-17 15:37:42 -0800126 private final MailJsBridge mJsBridge = new MailJsBridge();
127
Andy Huang17a9cde2012-03-09 18:03:16 -0800128 private final WebViewClient mWebViewClient = new ConversationWebViewClient();
129
Andy Huang7bdc3752012-03-25 17:18:19 -0700130 private ConversationViewAdapter mAdapter;
Andy Huang51067132012-03-12 20:08:19 -0700131
132 private boolean mViewsCreated;
Mark Wei4071c2f2012-09-26 14:38:38 -0700133 // True if we attempted to render before the views were laid out
134 // We will render immediately once layout is done
135 private boolean mNeedRender;
Andy Huang51067132012-03-12 20:08:19 -0700136
Andy Huang46dfba62012-04-19 01:47:32 -0700137 /**
138 * Temporary string containing the message bodies of the messages within a super-collapsed
139 * block, for one-time use during block expansion. We cannot easily pass the body HTML
140 * into JS without problematic escaping, so hold onto it momentarily and signal JS to fetch it
141 * using {@link MailJsBridge}.
142 */
143 private String mTempBodiesHtml;
144
Andy Huang632721e2012-04-11 16:57:26 -0700145 private int mMaxAutoLoadMessages;
146
Andy Huang9d3fd922012-09-26 22:23:58 -0700147 /**
148 * If this conversation fragment is not visible, and it's inappropriate to load up front,
149 * this is the reason we are waiting. This flag should be cleared once it's okay to load
150 * the conversation.
151 */
152 private int mLoadWaitReason = LOAD_NOW;
Andy Huang632721e2012-04-11 16:57:26 -0700153
mindyp3bcf1802012-09-09 11:17:00 -0700154 private boolean mEnableContentReadySignal;
Andy Huang28b7aee2012-08-20 20:27:32 -0700155
mindypdde3f9f2012-09-10 17:35:35 -0700156 private ContentSizeChangeListener mWebViewSizeChangeListener;
157
Andy Huange964eee2012-10-02 19:24:58 -0700158 private float mWebViewYPercent;
159
160 /**
161 * Has loadData been called on the WebView yet?
162 */
163 private boolean mWebViewLoadedData;
164
Andy Huang63b3c672012-10-05 19:27:28 -0700165 private long mWebViewLoadStartMs;
166
Andy Huang9d3fd922012-09-26 22:23:58 -0700167 private final DataSetObserver mLoadedObserver = new DataSetObserver() {
168 @Override
169 public void onChanged() {
170 getHandler().post(new FragmentRunnable("delayedConversationLoad") {
171 @Override
172 public void go() {
173 LogUtils.d(LOG_TAG, "CVF load observer fired, this=%s",
174 ConversationViewFragment.this);
175 handleDelayedConversationLoad();
176 }
177 });
178 }
179 };
Andy Huangf70fc402012-02-17 15:37:42 -0800180
Andy Huang30bcfe72012-10-18 18:09:03 -0700181 private final Runnable mOnProgressDismiss = new FragmentRunnable("onProgressDismiss") {
Andy Huang7d4746e2012-10-17 17:03:17 -0700182 @Override
183 public void go() {
184 if (isUserVisible()) {
185 onConversationSeen();
186 }
Andy Huang30bcfe72012-10-18 18:09:03 -0700187 mWebView.onRenderComplete();
Andy Huang7d4746e2012-10-17 17:03:17 -0700188 }
189 };
190
Andy Huangbd544e32012-05-29 15:56:51 -0700191 private static final boolean DEBUG_DUMP_CONVERSATION_HTML = false;
Andy Huang47aa9c92012-07-31 15:37:21 -0700192 private static final boolean DISABLE_OFFSCREEN_LOADING = false;
Andy Huang06c03622012-10-22 18:59:45 -0700193 private static final boolean DEBUG_DUMP_CURSOR_CONTENTS = false;
Andy Huange964eee2012-10-02 19:24:58 -0700194
195 private static final String BUNDLE_KEY_WEBVIEW_Y_PERCENT =
196 ConversationViewFragment.class.getName() + "webview-y-percent";
Andy Huangbd544e32012-05-29 15:56:51 -0700197
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800198 /**
199 * Constructor needs to be public to handle orientation changes and activity lifecycle events.
200 */
Andy Huangf70fc402012-02-17 15:37:42 -0800201 public ConversationViewFragment() {
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800202 super();
Mindy Pereira9b875682012-02-15 18:10:54 -0800203 }
204
205 /**
206 * Creates a new instance of {@link ConversationViewFragment}, initialized
Andy Huang632721e2012-04-11 16:57:26 -0700207 * to display a conversation with other parameters inherited/copied from an existing bundle,
208 * typically one created using {@link #makeBasicArgs}.
209 */
210 public static ConversationViewFragment newInstance(Bundle existingArgs,
211 Conversation conversation) {
212 ConversationViewFragment f = new ConversationViewFragment();
213 Bundle args = new Bundle(existingArgs);
214 args.putParcelable(ARG_CONVERSATION, conversation);
215 f.setArguments(args);
216 return f;
217 }
218
mindypf4fce122012-09-14 15:55:33 -0700219 @Override
Andy Huangadbf3e82012-10-13 13:30:19 -0700220 public void onAccountChanged(Account newAccount, Account oldAccount) {
221 // if overview mode has changed, re-render completely (no need to also update headers)
222 if (isOverviewMode(newAccount) != isOverviewMode(oldAccount)) {
223 setupOverviewMode();
224 final MessageCursor c = getMessageCursor();
225 if (c != null) {
226 renderConversation(c);
227 } else {
228 // Null cursor means this fragment is either waiting to load or in the middle of
229 // loading. Either way, a future render will happen anyway, and the new setting
230 // will take effect when that happens.
231 }
232 return;
233 }
234
mindypf4fce122012-09-14 15:55:33 -0700235 // settings may have been updated; refresh views that are known to
236 // depend on settings
mindypf4fce122012-09-14 15:55:33 -0700237 mAdapter.notifyDataSetChanged();
Andy Huang632721e2012-04-11 16:57:26 -0700238 }
239
Mindy Pereira9b875682012-02-15 18:10:54 -0800240 @Override
241 public void onActivityCreated(Bundle savedInstanceState) {
Andy Huang9d3fd922012-09-26 22:23:58 -0700242 LogUtils.d(LOG_TAG, "IN CVF.onActivityCreated, this=%s visible=%s", this, isUserVisible());
Mindy Pereira9b875682012-02-15 18:10:54 -0800243 super.onActivityCreated(savedInstanceState);
Mark Wei1abfcaf2012-09-27 11:11:07 -0700244
245 if (mActivity == null || mActivity.isFinishing()) {
246 // Activity is finishing, just bail.
247 return;
248 }
249
mindypf4fce122012-09-14 15:55:33 -0700250 Context context = getContext();
251 mTemplates = new HtmlConversationTemplates(context);
Andy Huang59e0b182012-08-14 14:32:23 -0700252
mindypf4fce122012-09-14 15:55:33 -0700253 final FormattedDateBuilder dateBuilder = new FormattedDateBuilder(context);
Andy Huang59e0b182012-08-14 14:32:23 -0700254
Paul Westbrook8081df42012-09-10 15:43:36 -0700255 mAdapter = new ConversationViewAdapter(mActivity, this,
mindypf4fce122012-09-14 15:55:33 -0700256 getLoaderManager(), this, getContactInfoSource(), this,
Paul Westbrook8081df42012-09-10 15:43:36 -0700257 this, mAddressCache, dateBuilder);
Andy Huang51067132012-03-12 20:08:19 -0700258 mConversationContainer.setOverlayAdapter(mAdapter);
259
Andy Huang59e0b182012-08-14 14:32:23 -0700260 // set up snap header (the adapter usually does this with the other ones)
261 final MessageHeaderView snapHeader = mConversationContainer.getSnapHeader();
Andy Huang28b7aee2012-08-20 20:27:32 -0700262 snapHeader.initialize(dateBuilder, this, mAddressCache);
Andy Huang59e0b182012-08-14 14:32:23 -0700263 snapHeader.setCallbacks(this);
mindypf4fce122012-09-14 15:55:33 -0700264 snapHeader.setContactInfoSource(getContactInfoSource());
Andy Huang59e0b182012-08-14 14:32:23 -0700265
Andy Huang632721e2012-04-11 16:57:26 -0700266 mMaxAutoLoadMessages = getResources().getInteger(R.integer.max_auto_load_messages);
267
mindypf4fce122012-09-14 15:55:33 -0700268 mWebView.setOnCreateContextMenuListener(new WebViewContextMenu(getActivity()));
Andy Huang0b7ed6f2012-07-25 19:23:26 -0700269
Andy Huangadbf3e82012-10-13 13:30:19 -0700270 // set this up here instead of onCreateView to ensure the latest Account is loaded
271 setupOverviewMode();
272
Andy Huang9d3fd922012-09-26 22:23:58 -0700273 // Defer the call to initLoader with a Handler.
274 // We want to wait until we know which fragments are present and their final visibility
275 // states before going off and doing work. This prevents extraneous loading from occurring
276 // as the ViewPager shifts about before the initial position is set.
277 //
278 // e.g. click on item #10
279 // ViewPager.setAdapter() actually first loads #0 and #1 under the assumption that #0 is
280 // the initial primary item
281 // Then CPC immediately sets the primary item to #10, which tears down #0/#1 and sets up
282 // #9/#10/#11.
283 getHandler().post(new FragmentRunnable("showConversation") {
284 @Override
285 public void go() {
286 showConversation();
287 }
288 });
Paul Westbrookcebcc642012-08-08 10:06:04 -0700289
290 if (mConversation.conversationBaseUri != null &&
Paul Westbrookb8361c92012-09-27 10:57:14 -0700291 !Utils.isEmpty(mAccount.accoutCookieQueryUri)) {
Paul Westbrookcebcc642012-08-08 10:06:04 -0700292 // Set the cookie for this base url
Paul Westbrookb8361c92012-09-27 10:57:14 -0700293 new SetCookieTask(getContext(), mConversation.conversationBaseUri,
294 mAccount.accoutCookieQueryUri).execute();
Paul Westbrookcebcc642012-08-08 10:06:04 -0700295 }
Mindy Pereira9b875682012-02-15 18:10:54 -0800296 }
297
Mindy Pereira9b875682012-02-15 18:10:54 -0800298 @Override
Andy Huange964eee2012-10-02 19:24:58 -0700299 public void onCreate(Bundle savedState) {
300 super.onCreate(savedState);
301
302 if (savedState != null) {
303 mWebViewYPercent = savedState.getFloat(BUNDLE_KEY_WEBVIEW_Y_PERCENT);
304 }
305 }
306
307 @Override
Mindy Pereira9b875682012-02-15 18:10:54 -0800308 public View onCreateView(LayoutInflater inflater,
309 ViewGroup container, Bundle savedInstanceState) {
Andy Huang839ada22012-07-20 15:48:40 -0700310
Andy Huang632721e2012-04-11 16:57:26 -0700311 View rootView = inflater.inflate(R.layout.conversation_view, container, false);
Andy Huangf70fc402012-02-17 15:37:42 -0800312 mConversationContainer = (ConversationContainer) rootView
313 .findViewById(R.id.conversation_container);
Andy Huang8f187782012-11-06 17:49:25 -0800314 mConversationContainer.setAccountController(this);
Andy Huang47aa9c92012-07-31 15:37:21 -0700315
316 mNewMessageBar = mConversationContainer.findViewById(R.id.new_message_notification_bar);
317 mNewMessageBar.setOnClickListener(new View.OnClickListener() {
318 @Override
319 public void onClick(View v) {
320 onNewMessageBarClick();
321 }
322 });
323
mindypff282d02012-09-17 10:33:02 -0700324 instantiateProgressIndicators(rootView);
mindyp3bcf1802012-09-09 11:17:00 -0700325
Andy Huang5ff63742012-03-16 20:30:23 -0700326 mWebView = (ConversationWebView) mConversationContainer.findViewById(R.id.webview);
Andy Huangf70fc402012-02-17 15:37:42 -0800327
Andy Huangf70fc402012-02-17 15:37:42 -0800328 mWebView.addJavascriptInterface(mJsBridge, "mail");
mindyp3bcf1802012-09-09 11:17:00 -0700329 // On JB or newer, we use the 'webkitAnimationStart' DOM event to signal load complete
330 // Below JB, try to speed up initial render by having the webview do supplemental draws to
331 // custom a software canvas.
mindypb941fdb2012-09-11 08:28:23 -0700332 // TODO(mindyp):
333 //PAGE READINESS SIGNAL FOR JELLYBEAN AND NEWER
334 // Notify the app on 'webkitAnimationStart' of a simple dummy element with a simple no-op
335 // animation that immediately runs on page load. The app uses this as a signal that the
336 // content is loaded and ready to draw, since WebView delays firing this event until the
337 // layers are composited and everything is ready to draw.
338 // This signal does not seem to be reliable, so just use the old method for now.
mindyp32d911f2012-09-24 15:14:22 -0700339 mEnableContentReadySignal = Utils.isRunningJellybeanOrLater();
mindypafc9b362012-09-25 09:20:47 -0700340 mWebView.setUseSoftwareLayer(!mEnableContentReadySignal);
Andy Huang30bcfe72012-10-18 18:09:03 -0700341 mWebView.onUserVisibilityChanged(isUserVisible());
Andy Huang17a9cde2012-03-09 18:03:16 -0800342 mWebView.setWebViewClient(mWebViewClient);
Andy Huangf70fc402012-02-17 15:37:42 -0800343 mWebView.setWebChromeClient(new WebChromeClient() {
344 @Override
345 public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
346 LogUtils.i(LOG_TAG, "JS: %s (%s:%d)", consoleMessage.message(),
347 consoleMessage.sourceId(), consoleMessage.lineNumber());
348 return true;
349 }
350 });
351
Andy Huang3233bff2012-03-20 19:38:45 -0700352 final WebSettings settings = mWebView.getSettings();
Andy Huangf70fc402012-02-17 15:37:42 -0800353
Mark Wei56d83852012-09-19 14:28:50 -0700354 mScrollIndicators = (ScrollIndicatorsView) rootView.findViewById(R.id.scroll_indicators);
355 mScrollIndicators.setSourceView(mWebView);
356
Andy Huangf70fc402012-02-17 15:37:42 -0800357 settings.setJavaScriptEnabled(true);
Andy Huangf70fc402012-02-17 15:37:42 -0800358
Andy Huangc319b552012-04-25 19:53:50 -0700359 final float fontScale = getResources().getConfiguration().fontScale;
Andy Huangba283732012-06-25 19:14:10 -0700360 final int desiredFontSizePx = getResources()
361 .getInteger(R.integer.conversation_desired_font_size_px);
362 final int unstyledFontSizePx = getResources()
363 .getInteger(R.integer.conversation_unstyled_font_size_px);
Andy Huangc319b552012-04-25 19:53:50 -0700364
Andy Huangba283732012-06-25 19:14:10 -0700365 int textZoom = settings.getTextZoom();
366 // apply a correction to the default body text style to get regular text to the size we want
367 textZoom = textZoom * desiredFontSizePx / unstyledFontSizePx;
368 // then apply any system font scaling
Andy Huangc319b552012-04-25 19:53:50 -0700369 textZoom = (int) (textZoom * fontScale);
370 settings.setTextZoom(textZoom);
371
Andy Huang51067132012-03-12 20:08:19 -0700372 mViewsCreated = true;
Andy Huange964eee2012-10-02 19:24:58 -0700373 mWebViewLoadedData = false;
Andy Huang51067132012-03-12 20:08:19 -0700374
Mindy Pereira9b875682012-02-15 18:10:54 -0800375 return rootView;
376 }
377
378 @Override
379 public void onDestroyView() {
Mindy Pereira9b875682012-02-15 18:10:54 -0800380 super.onDestroyView();
Andy Huang46dfba62012-04-19 01:47:32 -0700381 mConversationContainer.setOverlayAdapter(null);
382 mAdapter = null;
Andy Huang9d3fd922012-09-26 22:23:58 -0700383 resetLoadWaiting(); // be sure to unregister any active load observer
Andy Huang51067132012-03-12 20:08:19 -0700384 mViewsCreated = false;
Mindy Pereira9b875682012-02-15 18:10:54 -0800385 }
386
Andy Huange964eee2012-10-02 19:24:58 -0700387 @Override
388 public void onSaveInstanceState(Bundle outState) {
389 super.onSaveInstanceState(outState);
390
391 outState.putFloat(BUNDLE_KEY_WEBVIEW_Y_PERCENT, calculateScrollYPercent());
392 }
393
394 private float calculateScrollYPercent() {
395 float p;
396 int scrollY = mWebView.getScrollY();
397 int viewH = mWebView.getHeight();
398 int webH = (int) (mWebView.getContentHeight() * mWebView.getScale());
399
400 if (webH == 0 || webH <= viewH) {
401 p = 0;
402 } else if (scrollY + viewH >= webH) {
403 // The very bottom is a special case, it acts as a stronger anchor than the scroll top
404 // at that point.
405 p = 1.0f;
406 } else {
407 p = (float) scrollY / webH;
408 }
409 return p;
410 }
411
Andy Huang9d3fd922012-09-26 22:23:58 -0700412 private void resetLoadWaiting() {
413 if (mLoadWaitReason == LOAD_WAIT_FOR_INITIAL_CONVERSATION) {
414 getListController().unregisterConversationLoadedObserver(mLoadedObserver);
415 }
416 mLoadWaitReason = LOAD_NOW;
417 }
418
Andy Huang5ff63742012-03-16 20:30:23 -0700419 @Override
mindypf4fce122012-09-14 15:55:33 -0700420 protected void markUnread() {
Andy Huang839ada22012-07-20 15:48:40 -0700421 // Ignore unsafe calls made after a fragment is detached from an activity
422 final ControllableActivity activity = (ControllableActivity) getActivity();
423 if (activity == null) {
424 LogUtils.w(LOG_TAG, "ignoring markUnread for conv=%s", mConversation.id);
425 return;
426 }
427
Andy Huang28e31e22012-07-26 16:33:15 -0700428 if (mViewState == null) {
429 LogUtils.i(LOG_TAG, "ignoring markUnread for conv with no view state (%d)",
430 mConversation.id);
431 return;
432 }
Andy Huang839ada22012-07-20 15:48:40 -0700433 activity.getConversationUpdater().markConversationMessagesUnread(mConversation,
Vikram Aggarwal4a878b62012-07-31 15:09:25 -0700434 mViewState.getUnreadMessageUris(), mViewState.getConversationInfo());
Andy Huang839ada22012-07-20 15:48:40 -0700435 }
436
mindypf4fce122012-09-14 15:55:33 -0700437 @Override
438 public void onUserVisibleHintChanged() {
Andy Huang9d3fd922012-09-26 22:23:58 -0700439 final boolean userVisible = isUserVisible();
440
441 if (!userVisible) {
mindyp32d911f2012-09-24 15:14:22 -0700442 dismissLoadingStatus();
Andy Huang9d3fd922012-09-26 22:23:58 -0700443 } else if (mViewsCreated) {
444 if (getMessageCursor() != null) {
445 LogUtils.d(LOG_TAG, "Fragment is now user-visible, onConversationSeen: %s", this);
446 onConversationSeen();
447 } else if (isLoadWaiting()) {
448 LogUtils.d(LOG_TAG, "Fragment is now user-visible, showing conversation: %s", this);
449 handleDelayedConversationLoad();
450 }
Andy Huang632721e2012-04-11 16:57:26 -0700451 }
Andy Huang632721e2012-04-11 16:57:26 -0700452
Andy Huang30bcfe72012-10-18 18:09:03 -0700453 if (mWebView != null) {
454 mWebView.onUserVisibilityChanged(userVisible);
455 }
Andy Huangf8cf5462012-10-17 18:29:14 -0700456 }
457
Andy Huang9d3fd922012-09-26 22:23:58 -0700458 /**
459 * Will either call initLoader now to begin loading, or set {@link #mLoadWaitReason} and do
460 * nothing (in which case you should later call {@link #handleDelayedConversationLoad()}).
461 */
Mindy Pereira9b875682012-02-15 18:10:54 -0800462 private void showConversation() {
Andy Huang9d3fd922012-09-26 22:23:58 -0700463 final int reason;
464
465 if (isUserVisible()) {
466 LogUtils.i(LOG_TAG,
467 "SHOWCONV: CVF is user-visible, immediately loading conversation (%s)", this);
468 reason = LOAD_NOW;
469 } else {
470 final boolean disableOffscreenLoading = DISABLE_OFFSCREEN_LOADING
471 || (mConversation.isRemote
472 || mConversation.getNumMessages() > mMaxAutoLoadMessages);
473
474 // When not visible, we should not immediately load if either this conversation is
475 // too heavyweight, or if the main/initial conversation is busy loading.
476 if (disableOffscreenLoading) {
477 reason = LOAD_WAIT_UNTIL_VISIBLE;
478 LogUtils.i(LOG_TAG, "SHOWCONV: CVF waiting until visible to load (%s)", this);
479 } else if (getListController().isInitialConversationLoading()) {
480 reason = LOAD_WAIT_FOR_INITIAL_CONVERSATION;
481 LogUtils.i(LOG_TAG, "SHOWCONV: CVF waiting for initial to finish (%s)", this);
482 getListController().registerConversationLoadedObserver(mLoadedObserver);
483 } else {
484 LogUtils.i(LOG_TAG,
485 "SHOWCONV: CVF is not visible, but no reason to wait. loading now. (%s)",
486 this);
487 reason = LOAD_NOW;
488 }
Andy Huang632721e2012-04-11 16:57:26 -0700489 }
Andy Huang9d3fd922012-09-26 22:23:58 -0700490
491 mLoadWaitReason = reason;
492 if (mLoadWaitReason == LOAD_NOW) {
493 startConversationLoad();
494 }
495 }
496
497 private void handleDelayedConversationLoad() {
498 resetLoadWaiting();
499 startConversationLoad();
500 }
501
502 private void startConversationLoad() {
mindyp3bcf1802012-09-09 11:17:00 -0700503 mWebView.setVisibility(View.VISIBLE);
mindypf4fce122012-09-14 15:55:33 -0700504 getLoaderManager().initLoader(MESSAGE_LOADER, Bundle.EMPTY, getMessageLoaderCallbacks());
Andy Huang9d3fd922012-09-26 22:23:58 -0700505 if (isUserVisible()) {
Andy Huang5460ce42012-08-16 19:38:27 -0700506 final SubjectDisplayChanger sdc = mActivity.getSubjectDisplayChanger();
507 if (sdc != null) {
508 sdc.setSubject(mConversation.subject);
509 }
510 }
mindyp3bcf1802012-09-09 11:17:00 -0700511 // TODO(mindyp): don't show loading status for a previously rendered
512 // conversation. Ielieve this is better done by making sure don't show loading status
513 // until XX ms have passed without loading completed.
514 showLoadingStatus();
Mindy Pereira8e915722012-02-16 14:42:56 -0800515 }
516
Andy Huang7d4746e2012-10-17 17:03:17 -0700517 private void revealConversation() {
Andy Huang30bcfe72012-10-18 18:09:03 -0700518 dismissLoadingStatus(mOnProgressDismiss);
Andy Huang7d4746e2012-10-17 17:03:17 -0700519 }
520
Andy Huang9d3fd922012-09-26 22:23:58 -0700521 private boolean isLoadWaiting() {
522 return mLoadWaitReason != LOAD_NOW;
523 }
524
Andy Huang51067132012-03-12 20:08:19 -0700525 private void renderConversation(MessageCursor messageCursor) {
mindyp3bcf1802012-09-09 11:17:00 -0700526 final String convHtml = renderMessageBodies(messageCursor, mEnableContentReadySignal);
Andy Huangbd544e32012-05-29 15:56:51 -0700527
528 if (DEBUG_DUMP_CONVERSATION_HTML) {
529 java.io.FileWriter fw = null;
530 try {
531 fw = new java.io.FileWriter("/sdcard/conv" + mConversation.id
532 + ".html");
533 fw.write(convHtml);
534 } catch (java.io.IOException e) {
535 e.printStackTrace();
536 } finally {
537 if (fw != null) {
538 try {
539 fw.close();
540 } catch (java.io.IOException e) {
541 e.printStackTrace();
542 }
543 }
544 }
545 }
546
Andy Huange964eee2012-10-02 19:24:58 -0700547 // save off existing scroll position before re-rendering
548 if (mWebViewLoadedData) {
549 mWebViewYPercent = calculateScrollYPercent();
550 }
551
Andy Huangbd544e32012-05-29 15:56:51 -0700552 mWebView.loadDataWithBaseURL(mBaseUri, convHtml, "text/html", "utf-8", null);
Andy Huange964eee2012-10-02 19:24:58 -0700553 mWebViewLoadedData = true;
Andy Huang63b3c672012-10-05 19:27:28 -0700554 mWebViewLoadStartMs = SystemClock.uptimeMillis();
Andy Huang51067132012-03-12 20:08:19 -0700555 }
556
Andy Huang7bdc3752012-03-25 17:18:19 -0700557 /**
558 * Populate the adapter with overlay views (message headers, super-collapsed blocks, a
559 * conversation header), and return an HTML document with spacer divs inserted for all overlays.
560 *
561 */
mindyp3bcf1802012-09-09 11:17:00 -0700562 private String renderMessageBodies(MessageCursor messageCursor,
563 boolean enableContentReadySignal) {
Andy Huangf70fc402012-02-17 15:37:42 -0800564 int pos = -1;
Andy Huang632721e2012-04-11 16:57:26 -0700565
Andy Huang1ee96b22012-08-24 20:19:53 -0700566 LogUtils.d(LOG_TAG, "IN renderMessageBodies, fragment=%s", this);
Andy Huang7bdc3752012-03-25 17:18:19 -0700567 boolean allowNetworkImages = false;
568
Andy Huangc7543572012-04-03 15:34:29 -0700569 // TODO: re-use any existing adapter item state (expanded, details expanded, show pics)
Andy Huang28b7aee2012-08-20 20:27:32 -0700570
Andy Huang7bdc3752012-03-25 17:18:19 -0700571 // Walk through the cursor and build up an overlay adapter as you go.
572 // Each overlay has an entry in the adapter for easy scroll handling in the container.
573 // Items are not necessarily 1:1 in cursor and adapter because of super-collapsed blocks.
574 // When adding adapter items, also add their heights to help the container later determine
575 // overlay dimensions.
576
Andy Huangdb620fe2012-08-24 15:45:28 -0700577 // When re-rendering, prevent ConversationContainer from laying out overlays until after
578 // the new spacers are positioned by WebView.
579 mConversationContainer.invalidateSpacerGeometry();
580
Andy Huang7bdc3752012-03-25 17:18:19 -0700581 mAdapter.clear();
582
Andy Huang47aa9c92012-07-31 15:37:21 -0700583 // re-evaluate the message parts of the view state, since the messages may have changed
584 // since the previous render
585 final ConversationViewState prevState = mViewState;
586 mViewState = new ConversationViewState(prevState);
587
Andy Huang5ff63742012-03-16 20:30:23 -0700588 // N.B. the units of height for spacers are actually dp and not px because WebView assumes
Andy Huang2e9acfe2012-03-15 22:39:36 -0700589 // a pixel is an mdpi pixel, unless you set device-dpi.
Andy Huang5ff63742012-03-16 20:30:23 -0700590
Andy Huang7bdc3752012-03-25 17:18:19 -0700591 // add a single conversation header item
592 final int convHeaderPos = mAdapter.addConversationHeader(mConversation);
Andy Huang23014702012-07-09 12:50:36 -0700593 final int convHeaderPx = measureOverlayHeight(convHeaderPos);
Andy Huang5ff63742012-03-16 20:30:23 -0700594
Andy Huang256b35c2012-08-22 15:19:13 -0700595 final int sideMarginPx = getResources().getDimensionPixelOffset(
596 R.dimen.conversation_view_margin_side) + getResources().getDimensionPixelOffset(
597 R.dimen.conversation_message_content_margin_side);
598
599 mTemplates.startConversation(mWebView.screenPxToWebPx(sideMarginPx),
600 mWebView.screenPxToWebPx(convHeaderPx));
Andy Huang3233bff2012-03-20 19:38:45 -0700601
Andy Huang46dfba62012-04-19 01:47:32 -0700602 int collapsedStart = -1;
Andy Huang839ada22012-07-20 15:48:40 -0700603 ConversationMessage prevCollapsedMsg = null;
Andy Huang46dfba62012-04-19 01:47:32 -0700604 boolean prevSafeForImages = false;
605
Andy Huangf70fc402012-02-17 15:37:42 -0800606 while (messageCursor.moveToPosition(++pos)) {
Andy Huang839ada22012-07-20 15:48:40 -0700607 final ConversationMessage msg = messageCursor.getMessage();
Andy Huang46dfba62012-04-19 01:47:32 -0700608
Andy Huang3233bff2012-03-20 19:38:45 -0700609 // TODO: save/restore 'show pics' state
610 final boolean safeForImages = msg.alwaysShowImages /* || savedStateSaysSafe */;
611 allowNetworkImages |= safeForImages;
Andy Huang24055282012-03-27 17:37:06 -0700612
Paul Westbrook08098ec2012-08-12 15:30:28 -0700613 final Integer savedExpanded = prevState.getExpansionState(msg);
614 final int expandedState;
Andy Huang839ada22012-07-20 15:48:40 -0700615 if (savedExpanded != null) {
Andy Huang1ee96b22012-08-24 20:19:53 -0700616 if (ExpansionState.isSuperCollapsed(savedExpanded) && messageCursor.isLast()) {
617 // override saved state when this is now the new last message
618 // this happens to the second-to-last message when you discard a draft
619 expandedState = ExpansionState.EXPANDED;
620 } else {
621 expandedState = savedExpanded;
622 }
Andy Huang839ada22012-07-20 15:48:40 -0700623 } else {
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700624 // new messages that are not expanded default to being eligible for super-collapse
Paul Westbrook08098ec2012-08-12 15:30:28 -0700625 expandedState = (!msg.read || msg.starred || messageCursor.isLast()) ?
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700626 ExpansionState.EXPANDED : ExpansionState.SUPER_COLLAPSED;
Andy Huang839ada22012-07-20 15:48:40 -0700627 }
Paul Westbrook08098ec2012-08-12 15:30:28 -0700628 mViewState.setExpansionState(msg, expandedState);
Andy Huangc7543572012-04-03 15:34:29 -0700629
Andy Huang839ada22012-07-20 15:48:40 -0700630 // save off "read" state from the cursor
631 // later, the view may not match the cursor (e.g. conversation marked read on open)
Andy Huang423bea22012-08-21 12:00:49 -0700632 // however, if a previous state indicated this message was unread, trust that instead
633 // so "mark unread" marks all originally unread messages
634 mViewState.setReadState(msg, msg.read && !prevState.isUnread(msg));
Andy Huang839ada22012-07-20 15:48:40 -0700635
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700636 // We only want to consider this for inclusion in the super collapsed block if
637 // 1) The we don't have previous state about this message (The first time that the
638 // user opens a conversation)
639 // 2) The previously saved state for this message indicates that this message is
640 // in the super collapsed block.
641 if (ExpansionState.isSuperCollapsed(expandedState)) {
642 // contribute to a super-collapsed block that will be emitted just before the
643 // next expanded header
644 if (collapsedStart < 0) {
645 collapsedStart = pos;
Andy Huang46dfba62012-04-19 01:47:32 -0700646 }
Andy Huangcd5c5ee2012-08-12 19:03:51 -0700647 prevCollapsedMsg = msg;
648 prevSafeForImages = safeForImages;
649 continue;
Andy Huang46dfba62012-04-19 01:47:32 -0700650 }
Andy Huang24055282012-03-27 17:37:06 -0700651
Andy Huang46dfba62012-04-19 01:47:32 -0700652 // resolve any deferred decisions on previous collapsed items
653 if (collapsedStart >= 0) {
654 if (pos - collapsedStart == 1) {
655 // special-case for a single collapsed message: no need to super-collapse it
656 renderMessage(prevCollapsedMsg, false /* expanded */,
657 prevSafeForImages);
658 } else {
659 renderSuperCollapsedBlock(collapsedStart, pos - 1);
660 }
661 prevCollapsedMsg = null;
662 collapsedStart = -1;
663 }
Andy Huang7bdc3752012-03-25 17:18:19 -0700664
Paul Westbrook08098ec2012-08-12 15:30:28 -0700665 renderMessage(msg, ExpansionState.isExpanded(expandedState), safeForImages);
Mindy Pereira9b875682012-02-15 18:10:54 -0800666 }
Andy Huang3233bff2012-03-20 19:38:45 -0700667
668 mWebView.getSettings().setBlockNetworkImage(!allowNetworkImages);
669
Paul Westbrookcebcc642012-08-08 10:06:04 -0700670 // If the conversation has specified a base uri, use it here, use mBaseUri
671 final String conversationBaseUri = mConversation.conversationBaseUri != null ?
672 mConversation.conversationBaseUri.toString() : mBaseUri;
673 return mTemplates.endConversation(mBaseUri, conversationBaseUri, 320,
Andy Huangadbf3e82012-10-13 13:30:19 -0700674 mWebView.getViewportWidth(), enableContentReadySignal, isOverviewMode(mAccount));
Mindy Pereira9b875682012-02-15 18:10:54 -0800675 }
Mindy Pereira674afa42012-02-17 14:05:24 -0800676
Andy Huang46dfba62012-04-19 01:47:32 -0700677 private void renderSuperCollapsedBlock(int start, int end) {
678 final int blockPos = mAdapter.addSuperCollapsedBlock(start, end);
Andy Huang23014702012-07-09 12:50:36 -0700679 final int blockPx = measureOverlayHeight(blockPos);
680 mTemplates.appendSuperCollapsedHtml(start, mWebView.screenPxToWebPx(blockPx));
Andy Huang46dfba62012-04-19 01:47:32 -0700681 }
682
Andy Huang839ada22012-07-20 15:48:40 -0700683 private void renderMessage(ConversationMessage msg, boolean expanded,
684 boolean safeForImages) {
Andy Huang46dfba62012-04-19 01:47:32 -0700685 final int headerPos = mAdapter.addMessageHeader(msg, expanded);
686 final MessageHeaderItem headerItem = (MessageHeaderItem) mAdapter.getItem(headerPos);
687
688 final int footerPos = mAdapter.addMessageFooter(headerItem);
689
690 // Measure item header and footer heights to allocate spacers in HTML
691 // But since the views themselves don't exist yet, render each item temporarily into
692 // a host view for measurement.
Andy Huang23014702012-07-09 12:50:36 -0700693 final int headerPx = measureOverlayHeight(headerPos);
694 final int footerPx = measureOverlayHeight(footerPos);
Andy Huang46dfba62012-04-19 01:47:32 -0700695
Andy Huang256b35c2012-08-22 15:19:13 -0700696 mTemplates.appendMessageHtml(msg, expanded, safeForImages,
Andy Huang23014702012-07-09 12:50:36 -0700697 mWebView.screenPxToWebPx(headerPx), mWebView.screenPxToWebPx(footerPx));
Andy Huang46dfba62012-04-19 01:47:32 -0700698 }
699
700 private String renderCollapsedHeaders(MessageCursor cursor,
701 SuperCollapsedBlockItem blockToReplace) {
702 final List<ConversationOverlayItem> replacements = Lists.newArrayList();
703
704 mTemplates.reset();
705
Mark Wei2b24e992012-09-10 16:40:07 -0700706 // In devices with non-integral density multiplier, screen pixels translate to non-integral
707 // web pixels. Keep track of the error that occurs when we cast all heights to int
708 float error = 0f;
Andy Huang46dfba62012-04-19 01:47:32 -0700709 for (int i = blockToReplace.getStart(), end = blockToReplace.getEnd(); i <= end; i++) {
710 cursor.moveToPosition(i);
Andy Huang839ada22012-07-20 15:48:40 -0700711 final ConversationMessage msg = cursor.getMessage();
Andy Huang46dfba62012-04-19 01:47:32 -0700712 final MessageHeaderItem header = mAdapter.newMessageHeaderItem(msg,
713 false /* expanded */);
714 final MessageFooterItem footer = mAdapter.newMessageFooterItem(header);
715
Andy Huang23014702012-07-09 12:50:36 -0700716 final int headerPx = measureOverlayHeight(header);
717 final int footerPx = measureOverlayHeight(footer);
Mark Wei2b24e992012-09-10 16:40:07 -0700718 error += mWebView.screenPxToWebPxError(headerPx)
719 + mWebView.screenPxToWebPxError(footerPx);
720
721 // When the error becomes greater than 1 pixel, make the next header 1 pixel taller
722 int correction = 0;
723 if (error >= 1) {
724 correction = 1;
725 error -= 1;
726 }
Andy Huang46dfba62012-04-19 01:47:32 -0700727
Andy Huang256b35c2012-08-22 15:19:13 -0700728 mTemplates.appendMessageHtml(msg, false /* expanded */, msg.alwaysShowImages,
Mark Wei2b24e992012-09-10 16:40:07 -0700729 mWebView.screenPxToWebPx(headerPx) + correction,
730 mWebView.screenPxToWebPx(footerPx));
Andy Huang46dfba62012-04-19 01:47:32 -0700731 replacements.add(header);
732 replacements.add(footer);
Andy Huang839ada22012-07-20 15:48:40 -0700733
Paul Westbrook08098ec2012-08-12 15:30:28 -0700734 mViewState.setExpansionState(msg, ExpansionState.COLLAPSED);
Andy Huang46dfba62012-04-19 01:47:32 -0700735 }
736
737 mAdapter.replaceSuperCollapsedBlock(blockToReplace, replacements);
Andy Huang06c03622012-10-22 18:59:45 -0700738 mAdapter.notifyDataSetChanged();
Andy Huang46dfba62012-04-19 01:47:32 -0700739
740 return mTemplates.emit();
741 }
742
743 private int measureOverlayHeight(int position) {
744 return measureOverlayHeight(mAdapter.getItem(position));
745 }
746
Andy Huang7bdc3752012-03-25 17:18:19 -0700747 /**
Andy Huangb8331b42012-07-16 19:08:53 -0700748 * Measure the height of an adapter view by rendering an adapter item into a temporary
Andy Huang46dfba62012-04-19 01:47:32 -0700749 * host view, and asking the view to immediately measure itself. This method will reuse
Andy Huang7bdc3752012-03-25 17:18:19 -0700750 * a previous adapter view from {@link ConversationContainer}'s scrap views if one was generated
751 * earlier.
752 * <p>
Andy Huang46dfba62012-04-19 01:47:32 -0700753 * After measuring the height, this method also saves the height in the
754 * {@link ConversationOverlayItem} for later use in overlay positioning.
Andy Huang7bdc3752012-03-25 17:18:19 -0700755 *
Andy Huang46dfba62012-04-19 01:47:32 -0700756 * @param convItem adapter item with data to render and measure
Andy Huang23014702012-07-09 12:50:36 -0700757 * @return height of the rendered view in screen px
Andy Huang7bdc3752012-03-25 17:18:19 -0700758 */
Andy Huang46dfba62012-04-19 01:47:32 -0700759 private int measureOverlayHeight(ConversationOverlayItem convItem) {
Andy Huang7bdc3752012-03-25 17:18:19 -0700760 final int type = convItem.getType();
761
762 final View convertView = mConversationContainer.getScrapView(type);
Andy Huangb8331b42012-07-16 19:08:53 -0700763 final View hostView = mAdapter.getView(convItem, convertView, mConversationContainer,
764 true /* measureOnly */);
Andy Huang7bdc3752012-03-25 17:18:19 -0700765 if (convertView == null) {
766 mConversationContainer.addScrapView(type, hostView);
767 }
768
Andy Huang9875bb42012-04-04 20:36:21 -0700769 final int heightPx = mConversationContainer.measureOverlay(hostView);
Andy Huang7bdc3752012-03-25 17:18:19 -0700770 convItem.setHeight(heightPx);
Andy Huang9875bb42012-04-04 20:36:21 -0700771 convItem.markMeasurementValid();
Andy Huang7bdc3752012-03-25 17:18:19 -0700772
Andy Huang23014702012-07-09 12:50:36 -0700773 return heightPx;
Andy Huang7bdc3752012-03-25 17:18:19 -0700774 }
775
Andy Huang5ff63742012-03-16 20:30:23 -0700776 @Override
777 public void onConversationViewHeaderHeightChange(int newHeight) {
Mark Weiab2d9982012-09-25 13:06:17 -0700778 final int h = mWebView.screenPxToWebPx(newHeight);
779
780 mWebView.loadUrl(String.format("javascript:setConversationHeaderSpacerHeight(%s);", h));
Andy Huang5ff63742012-03-16 20:30:23 -0700781 }
782
Andy Huang3233bff2012-03-20 19:38:45 -0700783 // END conversation header callbacks
784
785 // START message header callbacks
786 @Override
Andy Huangc7543572012-04-03 15:34:29 -0700787 public void setMessageSpacerHeight(MessageHeaderItem item, int newSpacerHeightPx) {
788 mConversationContainer.invalidateSpacerGeometry();
789
790 // update message HTML spacer height
Andy Huang23014702012-07-09 12:50:36 -0700791 final int h = mWebView.screenPxToWebPx(newSpacerHeightPx);
792 LogUtils.i(LAYOUT_TAG, "setting HTML spacer h=%dwebPx (%dscreenPx)", h,
793 newSpacerHeightPx);
Vikram Aggarwal5349ce12012-09-24 14:12:40 -0700794 mWebView.loadUrl(String.format("javascript:setMessageHeaderSpacerHeight('%s', %s);",
Andy Huang014ea4c2012-09-25 14:50:54 -0700795 mTemplates.getMessageDomId(item.getMessage()), h));
Andy Huang3233bff2012-03-20 19:38:45 -0700796 }
797
798 @Override
Andy Huangc7543572012-04-03 15:34:29 -0700799 public void setMessageExpanded(MessageHeaderItem item, int newSpacerHeightPx) {
800 mConversationContainer.invalidateSpacerGeometry();
801
802 // show/hide the HTML message body and update the spacer height
Andy Huang23014702012-07-09 12:50:36 -0700803 final int h = mWebView.screenPxToWebPx(newSpacerHeightPx);
804 LogUtils.i(LAYOUT_TAG, "setting HTML spacer expanded=%s h=%dwebPx (%dscreenPx)",
805 item.isExpanded(), h, newSpacerHeightPx);
Vikram Aggarwal5349ce12012-09-24 14:12:40 -0700806 mWebView.loadUrl(String.format("javascript:setMessageBodyVisible('%s', %s, %s);",
Andy Huang014ea4c2012-09-25 14:50:54 -0700807 mTemplates.getMessageDomId(item.getMessage()), item.isExpanded(), h));
Andy Huang839ada22012-07-20 15:48:40 -0700808
Andy Huang014ea4c2012-09-25 14:50:54 -0700809 mViewState.setExpansionState(item.getMessage(),
Paul Westbrook08098ec2012-08-12 15:30:28 -0700810 item.isExpanded() ? ExpansionState.EXPANDED : ExpansionState.COLLAPSED);
Andy Huang3233bff2012-03-20 19:38:45 -0700811 }
812
813 @Override
814 public void showExternalResources(Message msg) {
815 mWebView.getSettings().setBlockNetworkImage(false);
816 mWebView.loadUrl("javascript:unblockImages('" + mTemplates.getMessageDomId(msg) + "');");
817 }
818 // END message header callbacks
Andy Huang5ff63742012-03-16 20:30:23 -0700819
Andy Huang46dfba62012-04-19 01:47:32 -0700820 @Override
821 public void onSuperCollapsedClick(SuperCollapsedBlockItem item) {
mindypf4fce122012-09-14 15:55:33 -0700822 MessageCursor cursor = getMessageCursor();
823 if (cursor == null || !mViewsCreated) {
Andy Huang46dfba62012-04-19 01:47:32 -0700824 return;
825 }
826
mindypf4fce122012-09-14 15:55:33 -0700827 mTempBodiesHtml = renderCollapsedHeaders(cursor, item);
Andy Huang46dfba62012-04-19 01:47:32 -0700828 mWebView.loadUrl("javascript:replaceSuperCollapsedBlock(" + item.getStart() + ")");
829 }
830
Andy Huang47aa9c92012-07-31 15:37:21 -0700831 private void showNewMessageNotification(NewMessagesInfo info) {
832 final TextView descriptionView = (TextView) mNewMessageBar.findViewById(
833 R.id.new_message_description);
834 descriptionView.setText(info.getNotificationText());
835 mNewMessageBar.setVisibility(View.VISIBLE);
836 }
837
838 private void onNewMessageBarClick() {
839 mNewMessageBar.setVisibility(View.GONE);
840
mindypf4fce122012-09-14 15:55:33 -0700841 renderConversation(getMessageCursor()); // mCursor is already up-to-date
842 // per onLoadFinished()
Andy Huang5fbda022012-02-28 18:22:03 -0800843 }
844
Andy Huangadbf3e82012-10-13 13:30:19 -0700845 private static OverlayPosition[] parsePositions(final String[] topArray,
846 final String[] bottomArray) {
847 final int len = topArray.length;
848 final OverlayPosition[] positions = new OverlayPosition[len];
Andy Huangb5078b22012-03-05 19:52:29 -0800849 for (int i = 0; i < len; i++) {
Andy Huangadbf3e82012-10-13 13:30:19 -0700850 positions[i] = new OverlayPosition(
851 Integer.parseInt(topArray[i]), Integer.parseInt(bottomArray[i]));
Andy Huangb5078b22012-03-05 19:52:29 -0800852 }
Andy Huangadbf3e82012-10-13 13:30:19 -0700853 return positions;
Andy Huangb5078b22012-03-05 19:52:29 -0800854 }
855
Andy Huang47aa9c92012-07-31 15:37:21 -0700856 @Override
857 public String toString() {
858 // log extra info at DEBUG level or finer
859 final String s = super.toString();
860 if (!LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG) || mConversation == null) {
861 return s;
862 }
863 return "(" + s + " subj=" + mConversation.subject + ")";
864 }
865
Andy Huang16174812012-08-16 16:40:35 -0700866 private Address getAddress(String rawFrom) {
867 Address addr = mAddressCache.get(rawFrom);
868 if (addr == null) {
869 addr = Address.getEmailAddress(rawFrom);
870 mAddressCache.put(rawFrom, addr);
871 }
872 return addr;
873 }
874
Andy Huang9d3fd922012-09-26 22:23:58 -0700875 private void ensureContentSizeChangeListener() {
876 if (mWebViewSizeChangeListener == null) {
877 mWebViewSizeChangeListener = new ConversationWebView.ContentSizeChangeListener() {
878 @Override
879 public void onHeightChange(int h) {
880 // When WebKit says the DOM height has changed, re-measure
881 // bodies and re-position their headers.
882 // This is separate from the typical JavaScript DOM change
883 // listeners because cases like NARROW_COLUMNS text reflow do not trigger DOM
884 // events.
885 mWebView.loadUrl("javascript:measurePositions();");
886 }
887 };
888 }
889 mWebView.setContentSizeChangeListener(mWebViewSizeChangeListener);
890 }
891
Andy Huangadbf3e82012-10-13 13:30:19 -0700892 private static boolean isOverviewMode(Account acct) {
893 return acct.settings.conversationViewMode == UIProvider.ConversationViewMode.OVERVIEW;
894 }
895
896 private void setupOverviewMode() {
897 final boolean overviewMode = isOverviewMode(mAccount);
898 final WebSettings settings = mWebView.getSettings();
Andy Huang06def562012-10-14 00:19:11 -0700899 settings.setUseWideViewPort(overviewMode);
Andy Huangadbf3e82012-10-13 13:30:19 -0700900 settings.setSupportZoom(overviewMode);
901 if (overviewMode) {
902 settings.setBuiltInZoomControls(true);
903 settings.setDisplayZoomControls(false);
904 }
905 }
906
Paul Westbrook542fec92012-09-18 14:47:51 -0700907 private class ConversationWebViewClient extends AbstractConversationWebViewClient {
Andy Huang17a9cde2012-03-09 18:03:16 -0800908 @Override
909 public void onPageFinished(WebView view, String url) {
Andy Huang9a8bc1e2012-10-23 19:48:25 -0700910 // Ignore unsafe calls made after a fragment is detached from an activity.
911 // This method needs to, for example, get at the loader manager, which needs
912 // the fragment to be added.
Andy Huangde56e972012-07-26 18:23:08 -0700913 final ControllableActivity activity = (ControllableActivity) getActivity();
Andy Huang9a8bc1e2012-10-23 19:48:25 -0700914 if (!isAdded() || !mViewsCreated) {
Andy Huangde56e972012-07-26 18:23:08 -0700915 LogUtils.i(LOG_TAG, "ignoring CVF.onPageFinished, url=%s fragment=%s", url,
Andy Huangb95da852012-07-18 14:16:58 -0700916 ConversationViewFragment.this);
917 return;
918 }
919
Andy Huang30bcfe72012-10-18 18:09:03 -0700920 LogUtils.i(LOG_TAG, "IN CVF.onPageFinished, url=%s fragment=%s wv=%s t=%sms", url,
921 ConversationViewFragment.this, view,
Andy Huang63b3c672012-10-05 19:27:28 -0700922 (SystemClock.uptimeMillis() - mWebViewLoadStartMs));
Andy Huang632721e2012-04-11 16:57:26 -0700923
Andy Huang9d3fd922012-09-26 22:23:58 -0700924 ensureContentSizeChangeListener();
925
mindyp3bcf1802012-09-09 11:17:00 -0700926 if (!mEnableContentReadySignal) {
Andy Huang7d4746e2012-10-17 17:03:17 -0700927 revealConversation();
mindyp3bcf1802012-09-09 11:17:00 -0700928 }
Andy Huang9d3fd922012-09-26 22:23:58 -0700929
Andy Huang9a8bc1e2012-10-23 19:48:25 -0700930 final Set<String> emailAddresses = Sets.newHashSet();
931 for (Address addr : mAddressCache.values()) {
932 emailAddresses.add(addr.getAddress());
Andy Huangb8331b42012-07-16 19:08:53 -0700933 }
Andy Huang9a8bc1e2012-10-23 19:48:25 -0700934 ContactLoaderCallbacks callbacks = getContactInfoSource();
935 getContactInfoSource().setSenders(emailAddresses);
936 getLoaderManager().restartLoader(CONTACT_LOADER, Bundle.EMPTY, callbacks);
Andy Huang17a9cde2012-03-09 18:03:16 -0800937 }
938
Andy Huangaf5d4e02012-03-19 19:02:12 -0700939 @Override
940 public boolean shouldOverrideUrlLoading(WebView view, String url) {
Paul Westbrook542fec92012-09-18 14:47:51 -0700941 return mViewsCreated && super.shouldOverrideUrlLoading(view, url);
Andy Huangaf5d4e02012-03-19 19:02:12 -0700942 }
Andy Huang17a9cde2012-03-09 18:03:16 -0800943 }
944
Andy Huangf70fc402012-02-17 15:37:42 -0800945 /**
946 * NOTE: all public methods must be listed in the proguard flags so that they can be accessed
947 * via reflection and not stripped.
948 *
949 */
950 private class MailJsBridge {
951
952 @SuppressWarnings("unused")
Mindy Pereira974c9662012-09-14 10:02:08 -0700953 @JavascriptInterface
Andy Huangadbf3e82012-10-13 13:30:19 -0700954 public void onWebContentGeometryChange(final String[] overlayTopStrs,
955 final String[] overlayBottomStrs) {
Andy Huang9a8bc1e2012-10-23 19:48:25 -0700956 getHandler().post(new FragmentRunnable("onWebContentGeometryChange") {
mindyp1b3cc472012-09-27 11:32:59 -0700957
Andy Huang9a8bc1e2012-10-23 19:48:25 -0700958 @Override
959 public void go() {
960 try {
Andy Huang46dfba62012-04-19 01:47:32 -0700961 if (!mViewsCreated) {
mindyp1b3cc472012-09-27 11:32:59 -0700962 LogUtils.d(LOG_TAG, "ignoring webContentGeometryChange because views"
963 + " are gone, %s", ConversationViewFragment.this);
Andy Huang46dfba62012-04-19 01:47:32 -0700964 return;
965 }
Andy Huangadbf3e82012-10-13 13:30:19 -0700966 mConversationContainer.onGeometryChange(
967 parsePositions(overlayTopStrs, overlayBottomStrs));
mindyp1b3cc472012-09-27 11:32:59 -0700968 if (mDiff != 0) {
969 // SCROLL!
970 int scale = (int) (mWebView.getScale() / mWebView.getInitialScale());
971 if (scale > 1) {
972 mWebView.scrollBy(0, (mDiff * (scale - 1)));
973 }
974 mDiff = 0;
975 }
Andy Huang9a8bc1e2012-10-23 19:48:25 -0700976 } catch (Throwable t) {
977 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onWebContentGeometryChange");
Andy Huang46dfba62012-04-19 01:47:32 -0700978 }
Andy Huang9a8bc1e2012-10-23 19:48:25 -0700979 }
980 });
Andy Huang46dfba62012-04-19 01:47:32 -0700981 }
982
983 @SuppressWarnings("unused")
Mindy Pereira974c9662012-09-14 10:02:08 -0700984 @JavascriptInterface
Andy Huang46dfba62012-04-19 01:47:32 -0700985 public String getTempMessageBodies() {
986 try {
987 if (!mViewsCreated) {
988 return "";
Andy Huangf70fc402012-02-17 15:37:42 -0800989 }
Andy Huang46dfba62012-04-19 01:47:32 -0700990
991 final String s = mTempBodiesHtml;
992 mTempBodiesHtml = null;
993 return s;
994 } catch (Throwable t) {
995 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getTempMessageBodies");
996 return "";
997 }
Andy Huangf70fc402012-02-17 15:37:42 -0800998 }
999
Andy Huang014ea4c2012-09-25 14:50:54 -07001000 @SuppressWarnings("unused")
1001 @JavascriptInterface
1002 public String getMessageBody(String domId) {
1003 try {
1004 final MessageCursor cursor = getMessageCursor();
1005 if (!mViewsCreated || cursor == null) {
1006 return "";
1007 }
1008
1009 int pos = -1;
1010 while (cursor.moveToPosition(++pos)) {
1011 final ConversationMessage msg = cursor.getMessage();
1012 if (TextUtils.equals(domId, mTemplates.getMessageDomId(msg))) {
1013 return msg.getBodyAsHtml();
1014 }
1015 }
1016
1017 return "";
1018
1019 } catch (Throwable t) {
1020 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getMessageBody");
1021 return "";
1022 }
1023 }
1024
mindyp3bcf1802012-09-09 11:17:00 -07001025 @SuppressWarnings("unused")
Mindy Pereira974c9662012-09-14 10:02:08 -07001026 @JavascriptInterface
mindyp3bcf1802012-09-09 11:17:00 -07001027 public void onContentReady() {
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001028 getHandler().post(new FragmentRunnable("onContentReady") {
1029 @Override
1030 public void go() {
1031 try {
Andy Huang63b3c672012-10-05 19:27:28 -07001032 if (mWebViewLoadStartMs != 0) {
1033 LogUtils.i(LOG_TAG, "IN CVF.onContentReady, f=%s vis=%s t=%sms",
1034 ConversationViewFragment.this,
1035 isUserVisible(),
1036 (SystemClock.uptimeMillis() - mWebViewLoadStartMs));
1037 }
Andy Huang7d4746e2012-10-17 17:03:17 -07001038 revealConversation();
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001039 } catch (Throwable t) {
1040 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onContentReady");
1041 // Still try to show the conversation.
1042 revealConversation();
mindyp3bcf1802012-09-09 11:17:00 -07001043 }
Andy Huang9a8bc1e2012-10-23 19:48:25 -07001044 }
1045 });
mindyp3bcf1802012-09-09 11:17:00 -07001046 }
Andy Huange964eee2012-10-02 19:24:58 -07001047
1048 @SuppressWarnings("unused")
1049 @JavascriptInterface
1050 public float getScrollYPercent() {
1051 try {
1052 return mWebViewYPercent;
1053 } catch (Throwable t) {
1054 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getScrollYPercent");
1055 return 0f;
1056 }
1057 }
Andy Huangf70fc402012-02-17 15:37:42 -08001058 }
1059
Andy Huang47aa9c92012-07-31 15:37:21 -07001060 private class NewMessagesInfo {
1061 int count;
Andy Huang06c03622012-10-22 18:59:45 -07001062 int countFromSelf;
Andy Huang47aa9c92012-07-31 15:37:21 -07001063 String senderAddress;
1064
1065 /**
1066 * Return the display text for the new message notification overlay. It will be formatted
1067 * appropriately for a single new message vs. multiple new messages.
1068 *
1069 * @return display text
1070 */
1071 public String getNotificationText() {
mindypad0c30d2012-09-25 12:09:13 -07001072 Resources res = getResources();
Andy Huang47aa9c92012-07-31 15:37:21 -07001073 if (count > 1) {
mindypad0c30d2012-09-25 12:09:13 -07001074 return res.getString(R.string.new_incoming_messages_many, count);
Andy Huang47aa9c92012-07-31 15:37:21 -07001075 } else {
Andy Huang16174812012-08-16 16:40:35 -07001076 final Address addr = getAddress(senderAddress);
mindypad0c30d2012-09-25 12:09:13 -07001077 return res.getString(R.string.new_incoming_messages_one,
1078 TextUtils.isEmpty(addr.getName()) ? addr.getAddress() : addr.getName());
Andy Huang47aa9c92012-07-31 15:37:21 -07001079 }
Andy Huang47aa9c92012-07-31 15:37:21 -07001080 }
1081 }
1082
mindypf4fce122012-09-14 15:55:33 -07001083 @Override
Andy Huang014ea4c2012-09-25 14:50:54 -07001084 public void onMessageCursorLoadFinished(Loader<Cursor> loader, MessageCursor newCursor,
1085 MessageCursor oldCursor) {
mindypf4fce122012-09-14 15:55:33 -07001086 /*
1087 * what kind of changes affect the MessageCursor? 1. new message(s) 2.
1088 * read/unread state change 3. deleted message, either regular or draft
1089 * 4. updated message, either from self or from others, updated in
1090 * content or state or sender 5. star/unstar of message (technically
1091 * similar to #1) 6. other label change Use MessageCursor.hashCode() to
1092 * sort out interesting vs. no-op cursor updates.
1093 */
Andy Huangb8331b42012-07-16 19:08:53 -07001094
Andy Huang233d4352012-10-18 14:00:24 -07001095 if (oldCursor != null && !oldCursor.isClosed()) {
Andy Huang014ea4c2012-09-25 14:50:54 -07001096 final NewMessagesInfo info = getNewIncomingMessagesInfo(newCursor);
Andy Huangb8331b42012-07-16 19:08:53 -07001097
Andy Huang014ea4c2012-09-25 14:50:54 -07001098 if (info.count > 0) {
1099 // don't immediately render new incoming messages from other
1100 // senders
1101 // (to avoid a new message from losing the user's focus)
1102 LogUtils.i(LOG_TAG, "CONV RENDER: conversation updated"
Andy Huang9d3fd922012-09-26 22:23:58 -07001103 + ", holding cursor for new incoming message (%s)", this);
Andy Huang014ea4c2012-09-25 14:50:54 -07001104 showNewMessageNotification(info);
1105 return;
1106 }
1107
Andy Huang06c03622012-10-22 18:59:45 -07001108 final int oldState = oldCursor.getStateHashCode();
1109 final boolean changed = newCursor.getStateHashCode() != oldState;
Andy Huang233d4352012-10-18 14:00:24 -07001110
Andy Huang014ea4c2012-09-25 14:50:54 -07001111 if (!changed) {
1112 final boolean processedInPlace = processInPlaceUpdates(newCursor, oldCursor);
1113 if (processedInPlace) {
Andy Huang9d3fd922012-09-26 22:23:58 -07001114 LogUtils.i(LOG_TAG, "CONV RENDER: processed update(s) in place (%s)", this);
Andy Huang1ee96b22012-08-24 20:19:53 -07001115 } else {
mindypf4fce122012-09-14 15:55:33 -07001116 LogUtils.i(LOG_TAG, "CONV RENDER: uninteresting update"
Andy Huang9d3fd922012-09-26 22:23:58 -07001117 + ", ignoring this conversation update (%s)", this);
Andy Huang1ee96b22012-08-24 20:19:53 -07001118 }
Andy Huangb8331b42012-07-16 19:08:53 -07001119 return;
Andy Huang06c03622012-10-22 18:59:45 -07001120 } else if (info.countFromSelf == 1) {
1121 // Special-case the very common case of a new cursor that is the same as the old
1122 // one, except that there is a new message from yourself. This happens upon send.
1123 final boolean sameExceptNewLast = newCursor.getStateHashCode(1) == oldState;
1124 if (sameExceptNewLast) {
1125 LogUtils.i(LOG_TAG, "CONV RENDER: update is a single new message from self"
1126 + " (%s)", this);
1127 newCursor.moveToLast();
1128 processNewOutgoingMessage(newCursor.getMessage());
1129 return;
1130 }
Andy Huangb8331b42012-07-16 19:08:53 -07001131 }
Andy Huang6766b6e2012-09-28 12:43:52 -07001132 // cursors are different, and not due to an incoming message. fall
1133 // through and render.
1134 LogUtils.i(LOG_TAG, "CONV RENDER: conversation updated"
1135 + ", but not due to incoming message. rendering. (%s)", this);
Andy Huang06c03622012-10-22 18:59:45 -07001136
1137 if (DEBUG_DUMP_CURSOR_CONTENTS) {
1138 LogUtils.i(LOG_TAG, "old cursor: %s", oldCursor.getDebugDump());
1139 LogUtils.i(LOG_TAG, "new cursor: %s", newCursor.getDebugDump());
1140 }
Andy Huang6766b6e2012-09-28 12:43:52 -07001141 } else {
1142 LogUtils.i(LOG_TAG, "CONV RENDER: initial render. (%s)", this);
Andy Huangb8331b42012-07-16 19:08:53 -07001143 }
1144
Mark Wei4071c2f2012-09-26 14:38:38 -07001145 // if layout hasn't happened, delay render
1146 // This is needed in addition to the showConversation() delay to speed
1147 // up rotation and restoration.
1148 if (mConversationContainer.getWidth() == 0) {
1149 mNeedRender = true;
1150 mConversationContainer.addOnLayoutChangeListener(this);
1151 } else {
1152 renderConversation(newCursor);
1153 }
Andy Huangb8331b42012-07-16 19:08:53 -07001154 }
1155
mindypf4fce122012-09-14 15:55:33 -07001156 private NewMessagesInfo getNewIncomingMessagesInfo(MessageCursor newCursor) {
1157 final NewMessagesInfo info = new NewMessagesInfo();
Andy Huangb8331b42012-07-16 19:08:53 -07001158
mindypf4fce122012-09-14 15:55:33 -07001159 int pos = -1;
1160 while (newCursor.moveToPosition(++pos)) {
1161 final Message m = newCursor.getMessage();
1162 if (!mViewState.contains(m)) {
1163 LogUtils.i(LOG_TAG, "conversation diff: found new msg: %s", m.uri);
Andy Huangb8331b42012-07-16 19:08:53 -07001164
Scott Kennedy8960f0a2012-11-07 15:35:50 -08001165 final Address from = getAddress(m.getFrom());
mindypf4fce122012-09-14 15:55:33 -07001166 // distinguish ours from theirs
1167 // new messages from the account owner should not trigger a
1168 // notification
1169 if (mAccount.ownsFromAddress(from.getAddress())) {
1170 LogUtils.i(LOG_TAG, "found message from self: %s", m.uri);
Andy Huang06c03622012-10-22 18:59:45 -07001171 info.countFromSelf++;
mindypf4fce122012-09-14 15:55:33 -07001172 continue;
1173 }
Andy Huangb8331b42012-07-16 19:08:53 -07001174
mindypf4fce122012-09-14 15:55:33 -07001175 info.count++;
Scott Kennedy8960f0a2012-11-07 15:35:50 -08001176 info.senderAddress = m.getFrom();
Andy Huangb8331b42012-07-16 19:08:53 -07001177 }
Andy Huangb8331b42012-07-16 19:08:53 -07001178 }
mindypf4fce122012-09-14 15:55:33 -07001179 return info;
Andy Huangb8331b42012-07-16 19:08:53 -07001180 }
1181
Andy Huang014ea4c2012-09-25 14:50:54 -07001182 private boolean processInPlaceUpdates(MessageCursor newCursor, MessageCursor oldCursor) {
1183 final Set<String> idsOfChangedBodies = Sets.newHashSet();
Andy Huang6b3d0d92012-10-30 15:46:48 -07001184 final List<Integer> changedOverlayPositions = Lists.newArrayList();
1185
Andy Huang014ea4c2012-09-25 14:50:54 -07001186 boolean changed = false;
1187
1188 int pos = 0;
1189 while (true) {
1190 if (!newCursor.moveToPosition(pos) || !oldCursor.moveToPosition(pos)) {
1191 break;
1192 }
1193
1194 final ConversationMessage newMsg = newCursor.getMessage();
1195 final ConversationMessage oldMsg = oldCursor.getMessage();
1196
Scott Kennedy8960f0a2012-11-07 15:35:50 -08001197 if (!TextUtils.equals(newMsg.getFrom(), oldMsg.getFrom()) ||
Andy Huang2a1e8e32012-10-23 18:54:57 -07001198 newMsg.isSending != oldMsg.isSending) {
Andy Huang6b3d0d92012-10-30 15:46:48 -07001199 mAdapter.updateItemsForMessage(newMsg, changedOverlayPositions);
Andy Huang2a1e8e32012-10-23 18:54:57 -07001200 LogUtils.i(LOG_TAG, "msg #%d (%d): detected from/sending change. isSending=%s",
1201 pos, newMsg.id, newMsg.isSending);
Andy Huang014ea4c2012-09-25 14:50:54 -07001202 }
1203
1204 // update changed message bodies in-place
1205 if (!TextUtils.equals(newMsg.bodyHtml, oldMsg.bodyHtml) ||
1206 !TextUtils.equals(newMsg.bodyText, oldMsg.bodyText)) {
1207 // maybe just set a flag to notify JS to re-request changed bodies
1208 idsOfChangedBodies.add('"' + mTemplates.getMessageDomId(newMsg) + '"');
1209 LogUtils.i(LOG_TAG, "msg #%d (%d): detected body change", pos, newMsg.id);
1210 }
1211
1212 pos++;
1213 }
1214
Andy Huang6b3d0d92012-10-30 15:46:48 -07001215
1216 if (!changedOverlayPositions.isEmpty()) {
Andy Huang06c03622012-10-22 18:59:45 -07001217 // notify once after the entire adapter is updated
Andy Huang6b3d0d92012-10-30 15:46:48 -07001218 mConversationContainer.onOverlayModelUpdate(changedOverlayPositions);
1219 changed = true;
Andy Huang06c03622012-10-22 18:59:45 -07001220 }
1221
Andy Huang014ea4c2012-09-25 14:50:54 -07001222 if (!idsOfChangedBodies.isEmpty()) {
1223 mWebView.loadUrl(String.format("javascript:replaceMessageBodies([%s]);",
1224 TextUtils.join(",", idsOfChangedBodies)));
1225 changed = true;
1226 }
1227
1228 return changed;
1229 }
1230
Andy Huang06c03622012-10-22 18:59:45 -07001231 private void processNewOutgoingMessage(ConversationMessage msg) {
1232 mTemplates.reset();
1233 // this method will add some items to mAdapter, but we deliberately want to avoid notifying
1234 // adapter listeners (i.e. ConversationContainer) until onWebContentGeometryChange is next
1235 // called, to prevent N+1 headers rendering with N message bodies.
1236 renderMessage(msg, true /* expanded */, msg.alwaysShowImages);
1237 mTempBodiesHtml = mTemplates.emit();
1238
1239 mViewState.setExpansionState(msg, ExpansionState.EXPANDED);
1240 // FIXME: should the provider set this as initial state?
1241 mViewState.setReadState(msg, false /* read */);
1242
Andy Huang91d782a2012-10-25 12:37:29 -07001243 // From now until the updated spacer geometry is returned, the adapter items are mismatched
1244 // with the existing spacers. Do not let them layout.
1245 mConversationContainer.invalidateSpacerGeometry();
1246
Andy Huang06c03622012-10-22 18:59:45 -07001247 mWebView.loadUrl("javascript:appendMessageHtml();");
1248 }
1249
Paul Westbrookcebcc642012-08-08 10:06:04 -07001250 private class SetCookieTask extends AsyncTask<Void, Void, Void> {
1251 final String mUri;
Paul Westbrookb8361c92012-09-27 10:57:14 -07001252 final Uri mAccountCookieQueryUri;
1253 final ContentResolver mResolver;
Paul Westbrookcebcc642012-08-08 10:06:04 -07001254
Paul Westbrookb8361c92012-09-27 10:57:14 -07001255 SetCookieTask(Context context, Uri baseUri, Uri accountCookieQueryUri) {
1256 mUri = baseUri.toString();
1257 mAccountCookieQueryUri = accountCookieQueryUri;
1258 mResolver = context.getContentResolver();
Paul Westbrookcebcc642012-08-08 10:06:04 -07001259 }
1260
1261 @Override
1262 public Void doInBackground(Void... args) {
Paul Westbrookb8361c92012-09-27 10:57:14 -07001263 // First query for the coookie string from the UI provider
1264 final Cursor cookieCursor = mResolver.query(mAccountCookieQueryUri,
1265 UIProvider.ACCOUNT_COOKIE_PROJECTION, null, null, null);
1266 if (cookieCursor == null) {
1267 return null;
1268 }
1269
1270 try {
1271 if (cookieCursor.moveToFirst()) {
1272 final String cookie = cookieCursor.getString(
1273 cookieCursor.getColumnIndex(UIProvider.AccountCookieColumns.COOKIE));
1274
1275 if (cookie != null) {
1276 final CookieSyncManager csm =
1277 CookieSyncManager.createInstance(getContext());
1278 CookieManager.getInstance().setCookie(mUri, cookie);
1279 csm.sync();
1280 }
1281 }
1282
1283 } finally {
1284 cookieCursor.close();
1285 }
1286
1287
Paul Westbrookcebcc642012-08-08 10:06:04 -07001288 return null;
1289 }
1290 }
mindyp36280f32012-09-09 16:11:23 -07001291
mindyp26d4d2d2012-09-18 17:30:32 -07001292 @Override
mindyp36280f32012-09-09 16:11:23 -07001293 public void onConversationUpdated(Conversation conv) {
1294 final ConversationViewHeader headerView = (ConversationViewHeader) mConversationContainer
1295 .findViewById(R.id.conversation_header);
mindypb2b98ba2012-09-24 14:13:58 -07001296 mConversation = conv;
mindyp9e0b2362012-09-09 16:31:21 -07001297 if (headerView != null) {
1298 headerView.onConversationUpdated(conv);
1299 }
mindyp36280f32012-09-09 16:11:23 -07001300 }
Mark Wei4071c2f2012-09-26 14:38:38 -07001301
1302 @Override
1303 public void onLayoutChange(View v, int left, int top, int right,
1304 int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
1305 boolean sizeChanged = mNeedRender
1306 && mConversationContainer.getWidth() != 0;
1307 if (sizeChanged) {
1308 mNeedRender = false;
1309 mConversationContainer.removeOnLayoutChangeListener(this);
1310 renderConversation(getMessageCursor());
1311 }
1312 }
mindyp1b3cc472012-09-27 11:32:59 -07001313
1314 @Override
1315 public void setMessageDetailsExpanded(MessageHeaderItem i, boolean expanded,
1316 int heightBefore) {
1317 mDiff = (expanded ? 1 : -1) * Math.abs(i.getHeight() - heightBefore);
1318 }
Mindy Pereira9b875682012-02-15 18:10:54 -08001319}