blob: 914cf9c009873a3f87e900d015dab6afbfd7b717 [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
20import android.app.Activity;
21import android.app.Fragment;
Mindy Pereira8e915722012-02-16 14:42:56 -080022import android.app.LoaderManager;
Andy Huangaf5d4e02012-03-19 19:02:12 -070023import android.content.ActivityNotFoundException;
Mindy Pereira9b875682012-02-15 18:10:54 -080024import android.content.Context;
Mindy Pereira465d03d2012-02-16 15:17:46 -080025import android.content.CursorLoader;
Andy Huangaf5d4e02012-03-19 19:02:12 -070026import android.content.Intent;
Mindy Pereira8e915722012-02-16 14:42:56 -080027import android.content.Loader;
Mindy Pereira9b875682012-02-15 18:10:54 -080028import android.database.Cursor;
Mindy Pereira465d03d2012-02-16 15:17:46 -080029import android.net.Uri;
Mindy Pereira9b875682012-02-15 18:10:54 -080030import android.os.Bundle;
Andy Huangf70fc402012-02-17 15:37:42 -080031import android.os.Handler;
Andy Huangaf5d4e02012-03-19 19:02:12 -070032import android.provider.Browser;
Mindy Pereira9b875682012-02-15 18:10:54 -080033import android.view.LayoutInflater;
Andy Huang5ff63742012-03-16 20:30:23 -070034import android.view.Menu;
35import android.view.MenuInflater;
36import android.view.MenuItem;
Mindy Pereira9b875682012-02-15 18:10:54 -080037import android.view.View;
38import android.view.ViewGroup;
Andy Huangf70fc402012-02-17 15:37:42 -080039import android.webkit.ConsoleMessage;
40import android.webkit.WebChromeClient;
41import android.webkit.WebSettings;
Andy Huang17a9cde2012-03-09 18:03:16 -080042import android.webkit.WebView;
43import android.webkit.WebViewClient;
Mindy Pereira9b875682012-02-15 18:10:54 -080044
Mindy Pereira9b875682012-02-15 18:10:54 -080045import com.android.mail.R;
Andy Huang5ff63742012-03-16 20:30:23 -070046import com.android.mail.browse.ConversationContainer;
Andy Huang46dfba62012-04-19 01:47:32 -070047import com.android.mail.browse.ConversationOverlayItem;
Andy Huang7bdc3752012-03-25 17:18:19 -070048import com.android.mail.browse.ConversationViewAdapter;
Andy Huang46dfba62012-04-19 01:47:32 -070049import com.android.mail.browse.ConversationViewAdapter.MessageFooterItem;
Andy Huang7bdc3752012-03-25 17:18:19 -070050import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem;
Andy Huang46dfba62012-04-19 01:47:32 -070051import com.android.mail.browse.ConversationViewAdapter.SuperCollapsedBlockItem;
Andy Huang5ff63742012-03-16 20:30:23 -070052import com.android.mail.browse.ConversationViewHeader;
53import com.android.mail.browse.ConversationWebView;
Andy Huang7bdc3752012-03-25 17:18:19 -070054import com.android.mail.browse.MessageCursor;
Andy Huangf0e95c32012-04-02 18:11:17 -070055import com.android.mail.browse.MessageFooterView;
Andy Huang3233bff2012-03-20 19:38:45 -070056import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks;
Andy Huang46dfba62012-04-19 01:47:32 -070057import com.android.mail.browse.SuperCollapsedBlock;
Mindy Pereira9b875682012-02-15 18:10:54 -080058import com.android.mail.providers.Account;
Andy Huang65fe28f2012-04-06 18:08:53 -070059import com.android.mail.providers.Address;
Mindy Pereira9b875682012-02-15 18:10:54 -080060import com.android.mail.providers.Conversation;
Mindy Pereira863e4412012-03-23 12:21:38 -070061import com.android.mail.providers.Folder;
Paul Westbrook2d82d612012-03-07 09:21:14 -080062import com.android.mail.providers.ListParams;
Andy Huangf70fc402012-02-17 15:37:42 -080063import com.android.mail.providers.Message;
Vikram Aggarwal7d816002012-04-17 17:06:41 -070064import com.android.mail.providers.Settings;
Mindy Pereira9b875682012-02-15 18:10:54 -080065import com.android.mail.providers.UIProvider;
Mindy Pereira863e4412012-03-23 12:21:38 -070066import com.android.mail.providers.UIProvider.AccountCapabilities;
67import com.android.mail.providers.UIProvider.FolderCapabilities;
Mindy Pereira9b875682012-02-15 18:10:54 -080068import com.android.mail.utils.LogUtils;
Andy Huang2e9acfe2012-03-15 22:39:36 -070069import com.android.mail.utils.Utils;
Andy Huang46dfba62012-04-19 01:47:32 -070070import com.google.common.collect.Lists;
Andy Huang65fe28f2012-04-06 18:08:53 -070071import com.google.common.collect.Maps;
72
Andy Huang46dfba62012-04-19 01:47:32 -070073import java.util.List;
Andy Huang65fe28f2012-04-06 18:08:53 -070074import java.util.Map;
Mindy Pereira9b875682012-02-15 18:10:54 -080075
Andy Huangf70fc402012-02-17 15:37:42 -080076
Mindy Pereira9b875682012-02-15 18:10:54 -080077/**
78 * The conversation view UI component.
79 */
Mindy Pereira8e915722012-02-16 14:42:56 -080080public final class ConversationViewFragment extends Fragment implements
Andy Huang5ff63742012-03-16 20:30:23 -070081 LoaderManager.LoaderCallbacks<Cursor>,
Andy Huang3233bff2012-03-20 19:38:45 -070082 ConversationViewHeader.ConversationViewHeaderCallbacks,
Andy Huang46dfba62012-04-19 01:47:32 -070083 MessageHeaderViewCallbacks,
84 SuperCollapsedBlock.OnClickListener {
Mindy Pereira8e915722012-02-16 14:42:56 -080085
Mindy Pereira9b875682012-02-15 18:10:54 -080086 private static final String LOG_TAG = new LogUtils().getLogTag();
87
Mindy Pereira8e915722012-02-16 14:42:56 -080088 private static final int MESSAGE_LOADER_ID = 0;
Mindy Pereira9b875682012-02-15 18:10:54 -080089
Mindy Pereira8e915722012-02-16 14:42:56 -080090 private ControllableActivity mActivity;
Mindy Pereira9b875682012-02-15 18:10:54 -080091
Andy Huangf70fc402012-02-17 15:37:42 -080092 private Context mContext;
93
94 private Conversation mConversation;
Mindy Pereira9b875682012-02-15 18:10:54 -080095
Andy Huangf70fc402012-02-17 15:37:42 -080096 private ConversationContainer mConversationContainer;
Mindy Pereira9b875682012-02-15 18:10:54 -080097
Andy Huangf70fc402012-02-17 15:37:42 -080098 private Account mAccount;
Mindy Pereira9b875682012-02-15 18:10:54 -080099
Andy Huangf70fc402012-02-17 15:37:42 -0800100 private ConversationWebView mWebView;
Mindy Pereira9b875682012-02-15 18:10:54 -0800101
Andy Huangf70fc402012-02-17 15:37:42 -0800102 private HtmlConversationTemplates mTemplates;
103
104 private String mBaseUri;
105
106 private final Handler mHandler = new Handler();
107
108 private final MailJsBridge mJsBridge = new MailJsBridge();
109
Andy Huang17a9cde2012-03-09 18:03:16 -0800110 private final WebViewClient mWebViewClient = new ConversationWebViewClient();
111
Andy Huang7bdc3752012-03-25 17:18:19 -0700112 private ConversationViewAdapter mAdapter;
113 private MessageCursor mCursor;
Andy Huang51067132012-03-12 20:08:19 -0700114
115 private boolean mViewsCreated;
116
Andy Huang5ff63742012-03-16 20:30:23 -0700117 private MenuItem mChangeFoldersMenuItem;
118
119 private float mDensity;
120
Mindy Pereira863e4412012-03-23 12:21:38 -0700121 private Folder mFolder;
122
Andy Huang65fe28f2012-04-06 18:08:53 -0700123 private final Map<String, Address> mAddressCache = Maps.newHashMap();
124
Andy Huang46dfba62012-04-19 01:47:32 -0700125 /**
126 * Temporary string containing the message bodies of the messages within a super-collapsed
127 * block, for one-time use during block expansion. We cannot easily pass the body HTML
128 * into JS without problematic escaping, so hold onto it momentarily and signal JS to fetch it
129 * using {@link MailJsBridge}.
130 */
131 private String mTempBodiesHtml;
132
Andy Huangf70fc402012-02-17 15:37:42 -0800133 private static final String ARG_ACCOUNT = "account";
134 private static final String ARG_CONVERSATION = "conversation";
Mindy Pereira863e4412012-03-23 12:21:38 -0700135 private static final String ARG_FOLDER = "folder";
Andy Huangf70fc402012-02-17 15:37:42 -0800136
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800137 /**
138 * Constructor needs to be public to handle orientation changes and activity lifecycle events.
139 */
Andy Huangf70fc402012-02-17 15:37:42 -0800140 public ConversationViewFragment() {
Vikram Aggarwal6c511582012-02-27 10:59:47 -0800141 super();
Mindy Pereira9b875682012-02-15 18:10:54 -0800142 }
143
144 /**
145 * Creates a new instance of {@link ConversationViewFragment}, initialized
146 * to display conversation.
147 */
Andy Huangf70fc402012-02-17 15:37:42 -0800148 public static ConversationViewFragment newInstance(Account account,
Mindy Pereira863e4412012-03-23 12:21:38 -0700149 Conversation conversation, Folder folder) {
Andy Huangf70fc402012-02-17 15:37:42 -0800150 ConversationViewFragment f = new ConversationViewFragment();
151 Bundle args = new Bundle();
152 args.putParcelable(ARG_ACCOUNT, account);
153 args.putParcelable(ARG_CONVERSATION, conversation);
Mindy Pereira863e4412012-03-23 12:21:38 -0700154 args.putParcelable(ARG_FOLDER, folder);
Andy Huangf70fc402012-02-17 15:37:42 -0800155 f.setArguments(args);
156 return f;
Mindy Pereira9b875682012-02-15 18:10:54 -0800157 }
158
159 @Override
160 public void onActivityCreated(Bundle savedInstanceState) {
161 super.onActivityCreated(savedInstanceState);
162 // Strictly speaking, we get back an android.app.Activity from getActivity. However, the
163 // only activity creating a ConversationListContext is a MailActivity which is of type
164 // ControllableActivity, so this cast should be safe. If this cast fails, some other
165 // activity is creating ConversationListFragments. This activity must be of type
166 // ControllableActivity.
167 final Activity activity = getActivity();
Mindy Pereira863e4412012-03-23 12:21:38 -0700168 if (!(activity instanceof ControllableActivity)) {
Andy Huangf70fc402012-02-17 15:37:42 -0800169 LogUtils.wtf(LOG_TAG, "ConversationViewFragment expects only a ControllableActivity to"
170 + "create it. Cannot proceed.");
Mindy Pereira9b875682012-02-15 18:10:54 -0800171 }
172 mActivity = (ControllableActivity) activity;
Andy Huangf70fc402012-02-17 15:37:42 -0800173 mContext = mActivity.getApplicationContext();
Mindy Pereira9b875682012-02-15 18:10:54 -0800174 if (mActivity.isFinishing()) {
175 // Activity is finishing, just bail.
176 return;
177 }
Andy Huangf70fc402012-02-17 15:37:42 -0800178 mTemplates = new HtmlConversationTemplates(mContext);
Andy Huang51067132012-03-12 20:08:19 -0700179
Andy Huang7bdc3752012-03-25 17:18:19 -0700180 mAdapter = new ConversationViewAdapter(mActivity.getActivityContext(), mAccount,
Andy Huang46dfba62012-04-19 01:47:32 -0700181 getLoaderManager(), this, this, this, mAddressCache);
Andy Huang51067132012-03-12 20:08:19 -0700182 mConversationContainer.setOverlayAdapter(mAdapter);
183
Andy Huang5ff63742012-03-16 20:30:23 -0700184 mDensity = getResources().getDisplayMetrics().density;
185
Mindy Pereira9b875682012-02-15 18:10:54 -0800186 // Show conversation and start loading messages.
187 showConversation();
188 }
189
190 @Override
191 public void onCreate(Bundle savedState) {
192 LogUtils.v(LOG_TAG, "onCreate in FolderListFragment(this=%s)", this);
193 super.onCreate(savedState);
Andy Huangf70fc402012-02-17 15:37:42 -0800194
195 Bundle args = getArguments();
196 mAccount = args.getParcelable(ARG_ACCOUNT);
197 mConversation = args.getParcelable(ARG_CONVERSATION);
Mindy Pereira863e4412012-03-23 12:21:38 -0700198 mFolder = args.getParcelable(ARG_FOLDER);
Andy Huangf70fc402012-02-17 15:37:42 -0800199 mBaseUri = "x-thread://" + mAccount.name + "/" + mConversation.id;
Andy Huang5ff63742012-03-16 20:30:23 -0700200
201 // not really, we just want to get a crack to store a reference to the change_folders item
202 setHasOptionsMenu(true);
Mindy Pereira9b875682012-02-15 18:10:54 -0800203 }
204
205 @Override
206 public View onCreateView(LayoutInflater inflater,
207 ViewGroup container, Bundle savedInstanceState) {
208 View rootView = inflater.inflate(R.layout.conversation_view, null);
Andy Huangf70fc402012-02-17 15:37:42 -0800209 mConversationContainer = (ConversationContainer) rootView
210 .findViewById(R.id.conversation_container);
Andy Huang5ff63742012-03-16 20:30:23 -0700211 mWebView = (ConversationWebView) mConversationContainer.findViewById(R.id.webview);
Andy Huangf70fc402012-02-17 15:37:42 -0800212
Andy Huangf70fc402012-02-17 15:37:42 -0800213 mWebView.addJavascriptInterface(mJsBridge, "mail");
Andy Huang17a9cde2012-03-09 18:03:16 -0800214 mWebView.setWebViewClient(mWebViewClient);
Andy Huangf70fc402012-02-17 15:37:42 -0800215 mWebView.setWebChromeClient(new WebChromeClient() {
216 @Override
217 public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
218 LogUtils.i(LOG_TAG, "JS: %s (%s:%d)", consoleMessage.message(),
219 consoleMessage.sourceId(), consoleMessage.lineNumber());
220 return true;
221 }
222 });
223
Andy Huang3233bff2012-03-20 19:38:45 -0700224 final WebSettings settings = mWebView.getSettings();
Andy Huangf70fc402012-02-17 15:37:42 -0800225
226 settings.setJavaScriptEnabled(true);
227 settings.setUseWideViewPort(true);
228
229 settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL);
230
231 settings.setSupportZoom(true);
232 settings.setBuiltInZoomControls(true);
233 settings.setDisplayZoomControls(false);
Mindy Pereira9b875682012-02-15 18:10:54 -0800234
Andy Huang51067132012-03-12 20:08:19 -0700235 mViewsCreated = true;
236
Mindy Pereira9b875682012-02-15 18:10:54 -0800237 return rootView;
238 }
239
240 @Override
241 public void onDestroyView() {
Mindy Pereira9b875682012-02-15 18:10:54 -0800242 super.onDestroyView();
Andy Huang46dfba62012-04-19 01:47:32 -0700243 mConversationContainer.setOverlayAdapter(null);
244 mAdapter = null;
Andy Huang51067132012-03-12 20:08:19 -0700245 mViewsCreated = false;
Mindy Pereira9b875682012-02-15 18:10:54 -0800246 }
247
Andy Huang5ff63742012-03-16 20:30:23 -0700248 @Override
249 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
250 super.onCreateOptionsMenu(menu, inflater);
251
252 mChangeFoldersMenuItem = menu.findItem(R.id.change_folders);
253 }
254
Andy Huang5a929072012-03-23 20:17:10 -0700255 @Override
Mindy Pereirac9d59182012-03-22 16:06:46 -0700256 public void onPrepareOptionsMenu(Menu menu) {
257 super.onPrepareOptionsMenu(menu);
258 boolean showMarkImportant = !mConversation.isImportant();
Mindy Pereira863e4412012-03-23 12:21:38 -0700259 Utils.setMenuItemVisibility(
260 menu,
261 R.id.mark_important,
262 showMarkImportant
263 && mAccount
264 .supportsCapability(UIProvider.AccountCapabilities.MARK_IMPORTANT));
265 Utils.setMenuItemVisibility(
266 menu,
267 R.id.mark_not_important,
268 !showMarkImportant
269 && mAccount
270 .supportsCapability(UIProvider.AccountCapabilities.MARK_IMPORTANT));
271 // TODO(mindyp) show/ hide spam and mute based on conversation
272 // properties to be added.
273 Utils.setMenuItemVisibility(menu, R.id.y_button,
274 mAccount.supportsCapability(AccountCapabilities.ARCHIVE) && mFolder != null
275 && mFolder.supportsCapability(FolderCapabilities.ARCHIVE));
276 Utils.setMenuItemVisibility(menu, R.id.report_spam,
277 mAccount.supportsCapability(AccountCapabilities.REPORT_SPAM) && mFolder != null
278 && mFolder.supportsCapability(FolderCapabilities.REPORT_SPAM)
279 && !mConversation.spam);
280 Utils.setMenuItemVisibility(
281 menu,
282 R.id.mute,
283 mAccount.supportsCapability(AccountCapabilities.MUTE) && mFolder != null
284 && mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE)
285 && !mConversation.muted);
Mindy Pereirac9d59182012-03-22 16:06:46 -0700286 }
Mindy Pereira9b875682012-02-15 18:10:54 -0800287 /**
288 * Handles a request to show a new conversation list, either from a search query or for viewing
Mindy Pereira30fd47b2012-03-09 09:24:00 -0800289 * a folder. This will initiate a data load, and hence must be called on the UI thread.
Mindy Pereira9b875682012-02-15 18:10:54 -0800290 */
291 private void showConversation() {
Mindy Pereira8e915722012-02-16 14:42:56 -0800292 getLoaderManager().initLoader(MESSAGE_LOADER_ID, Bundle.EMPTY, this);
293 }
294
295 @Override
296 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
Andy Huang5fbda022012-02-28 18:22:03 -0800297 return new MessageLoader(mContext, mConversation.messageListUri);
Mindy Pereira8e915722012-02-16 14:42:56 -0800298 }
299
300 @Override
301 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
Andy Huang5fbda022012-02-28 18:22:03 -0800302 MessageCursor messageCursor = (MessageCursor) data;
Andy Huang51067132012-03-12 20:08:19 -0700303
Andy Huang3852f872012-03-27 19:35:11 -0700304 // TODO: handle Gmail loading states (like LOADING and ERROR)
305 if (messageCursor.getCount() == 0) {
Andy Huang7bdc3752012-03-25 17:18:19 -0700306 if (mCursor != null) {
Andy Huang3852f872012-03-27 19:35:11 -0700307 // TODO: need to exit this view- conversation may have been deleted, or for
308 // whatever reason is now invalid
309 } else {
310 // ignore zero-sized cursors during initial load
311 }
312 return;
Andy Huang51067132012-03-12 20:08:19 -0700313 }
Andy Huang3852f872012-03-27 19:35:11 -0700314
315 renderConversation(messageCursor);
Mindy Pereira9b875682012-02-15 18:10:54 -0800316 }
317
Mindy Pereira8e915722012-02-16 14:42:56 -0800318 @Override
319 public void onLoaderReset(Loader<Cursor> loader) {
Andy Huang7bdc3752012-03-25 17:18:19 -0700320 mCursor = null;
321 // TODO: null out all Message.mMessageCursor references
Andy Huang51067132012-03-12 20:08:19 -0700322 }
323
324 private void renderConversation(MessageCursor messageCursor) {
325 mWebView.loadDataWithBaseURL(mBaseUri, renderMessageBodies(messageCursor), "text/html",
326 "utf-8", null);
Andy Huang7bdc3752012-03-25 17:18:19 -0700327 mCursor = messageCursor;
Andy Huang51067132012-03-12 20:08:19 -0700328 }
329
330 private void updateConversation(MessageCursor messageCursor) {
331 // TODO: handle server-side conversation updates
332 // for simple things like header data changes, just re-render the affected headers
333 // if a new message is present, save off the pending cursor and show a notification to
334 // re-render
335
Andy Huang7bdc3752012-03-25 17:18:19 -0700336 mCursor = messageCursor;
Mindy Pereira8e915722012-02-16 14:42:56 -0800337 }
338
Andy Huang7bdc3752012-03-25 17:18:19 -0700339 /**
340 * Populate the adapter with overlay views (message headers, super-collapsed blocks, a
341 * conversation header), and return an HTML document with spacer divs inserted for all overlays.
342 *
343 */
Andy Huangf70fc402012-02-17 15:37:42 -0800344 private String renderMessageBodies(MessageCursor messageCursor) {
345 int pos = -1;
Andy Huang7bdc3752012-03-25 17:18:19 -0700346 boolean allowNetworkImages = false;
347
Andy Huangc7543572012-04-03 15:34:29 -0700348 // TODO: re-use any existing adapter item state (expanded, details expanded, show pics)
Vikram Aggarwal7d816002012-04-17 17:06:41 -0700349 final Settings settings = mActivity.getSettings();
350 if (settings != null) {
351 mAdapter.setDefaultReplyAll(settings.replyBehavior ==
352 UIProvider.DefaultReplyBehavior.REPLY_ALL);
353 }
Andy Huang7bdc3752012-03-25 17:18:19 -0700354 // Walk through the cursor and build up an overlay adapter as you go.
355 // Each overlay has an entry in the adapter for easy scroll handling in the container.
356 // Items are not necessarily 1:1 in cursor and adapter because of super-collapsed blocks.
357 // When adding adapter items, also add their heights to help the container later determine
358 // overlay dimensions.
359
360 mAdapter.clear();
361
Andy Huangf0e95c32012-04-02 18:11:17 -0700362 // We don't need to kick off attachment loaders during this first measurement phase,
363 // so disable them temporarily.
364 MessageFooterView.enableAttachmentLoaders(false);
365
Andy Huang5ff63742012-03-16 20:30:23 -0700366 // N.B. the units of height for spacers are actually dp and not px because WebView assumes
Andy Huang2e9acfe2012-03-15 22:39:36 -0700367 // a pixel is an mdpi pixel, unless you set device-dpi.
Andy Huang5ff63742012-03-16 20:30:23 -0700368
Andy Huang7bdc3752012-03-25 17:18:19 -0700369 // add a single conversation header item
370 final int convHeaderPos = mAdapter.addConversationHeader(mConversation);
371 final int convHeaderDp = measureOverlayHeight(convHeaderPos);
Andy Huang5ff63742012-03-16 20:30:23 -0700372
Andy Huang7bdc3752012-03-25 17:18:19 -0700373 mTemplates.startConversation(convHeaderDp);
Andy Huang3233bff2012-03-20 19:38:45 -0700374
Andy Huang46dfba62012-04-19 01:47:32 -0700375 int collapsedStart = -1;
376 Message prevCollapsedMsg = null;
377 boolean prevSafeForImages = false;
378
Andy Huangf70fc402012-02-17 15:37:42 -0800379 while (messageCursor.moveToPosition(++pos)) {
Marc Blankbec51152012-03-22 19:27:34 -0700380 final Message msg = messageCursor.getMessage();
Andy Huang46dfba62012-04-19 01:47:32 -0700381
Andy Huang3233bff2012-03-20 19:38:45 -0700382 // TODO: save/restore 'show pics' state
383 final boolean safeForImages = msg.alwaysShowImages /* || savedStateSaysSafe */;
384 allowNetworkImages |= safeForImages;
Andy Huang24055282012-03-27 17:37:06 -0700385
Andy Huangc7543572012-04-03 15:34:29 -0700386 final boolean expanded = !msg.read || msg.starred || messageCursor.isLast();
387
Andy Huang46dfba62012-04-19 01:47:32 -0700388 if (!expanded) {
389 // contribute to a super-collapsed block that will be emitted just before the next
390 // expanded header
391 if (collapsedStart < 0) {
392 collapsedStart = pos;
393 }
394 prevCollapsedMsg = msg;
395 prevSafeForImages = safeForImages;
396 continue;
397 }
Andy Huang24055282012-03-27 17:37:06 -0700398
Andy Huang46dfba62012-04-19 01:47:32 -0700399 // resolve any deferred decisions on previous collapsed items
400 if (collapsedStart >= 0) {
401 if (pos - collapsedStart == 1) {
402 // special-case for a single collapsed message: no need to super-collapse it
403 renderMessage(prevCollapsedMsg, false /* expanded */,
404 prevSafeForImages);
405 } else {
406 renderSuperCollapsedBlock(collapsedStart, pos - 1);
407 }
408 prevCollapsedMsg = null;
409 collapsedStart = -1;
410 }
Andy Huang7bdc3752012-03-25 17:18:19 -0700411
Andy Huang46dfba62012-04-19 01:47:32 -0700412 renderMessage(msg, expanded, safeForImages);
Mindy Pereira9b875682012-02-15 18:10:54 -0800413 }
Andy Huang3233bff2012-03-20 19:38:45 -0700414
Andy Huangf0e95c32012-04-02 18:11:17 -0700415 // Re-enable attachment loaders
416 MessageFooterView.enableAttachmentLoaders(true);
417
Andy Huang3233bff2012-03-20 19:38:45 -0700418 mWebView.getSettings().setBlockNetworkImage(!allowNetworkImages);
419
Andy Huangf70fc402012-02-17 15:37:42 -0800420 return mTemplates.endConversation(mBaseUri, 320);
Mindy Pereira9b875682012-02-15 18:10:54 -0800421 }
Mindy Pereira674afa42012-02-17 14:05:24 -0800422
Andy Huang46dfba62012-04-19 01:47:32 -0700423 private void renderSuperCollapsedBlock(int start, int end) {
424 final int blockPos = mAdapter.addSuperCollapsedBlock(start, end);
425 final int blockDp = measureOverlayHeight(blockPos);
426 mTemplates.appendSuperCollapsedHtml(start, blockDp);
427 }
428
429 private void renderMessage(Message msg, boolean expanded, boolean safeForImages) {
430 final int headerPos = mAdapter.addMessageHeader(msg, expanded);
431 final MessageHeaderItem headerItem = (MessageHeaderItem) mAdapter.getItem(headerPos);
432
433 final int footerPos = mAdapter.addMessageFooter(headerItem);
434
435 // Measure item header and footer heights to allocate spacers in HTML
436 // But since the views themselves don't exist yet, render each item temporarily into
437 // a host view for measurement.
438 final int headerDp = measureOverlayHeight(headerPos);
439 final int footerDp = measureOverlayHeight(footerPos);
440
441 mTemplates.appendMessageHtml(msg, expanded, safeForImages, 1.0f, headerDp,
442 footerDp);
443 }
444
445 private String renderCollapsedHeaders(MessageCursor cursor,
446 SuperCollapsedBlockItem blockToReplace) {
447 final List<ConversationOverlayItem> replacements = Lists.newArrayList();
448
449 mTemplates.reset();
450
451 for (int i = blockToReplace.getStart(), end = blockToReplace.getEnd(); i <= end; i++) {
452 cursor.moveToPosition(i);
453 final Message msg = cursor.getMessage();
454 final MessageHeaderItem header = mAdapter.newMessageHeaderItem(msg,
455 false /* expanded */);
456 final MessageFooterItem footer = mAdapter.newMessageFooterItem(header);
457
458 final int headerDp = measureOverlayHeight(header);
459 final int footerDp = measureOverlayHeight(footer);
460
461 mTemplates.appendMessageHtml(msg, false /* expanded */, msg.alwaysShowImages, 1.0f,
462 headerDp, footerDp);
463 replacements.add(header);
464 replacements.add(footer);
465 }
466
467 mAdapter.replaceSuperCollapsedBlock(blockToReplace, replacements);
468
469 return mTemplates.emit();
470 }
471
472 private int measureOverlayHeight(int position) {
473 return measureOverlayHeight(mAdapter.getItem(position));
474 }
475
Andy Huang7bdc3752012-03-25 17:18:19 -0700476 /**
Andy Huang46dfba62012-04-19 01:47:32 -0700477 * Measure the height of an adapter view by rendering and adapter item into a temporary
478 * host view, and asking the view to immediately measure itself. This method will reuse
Andy Huang7bdc3752012-03-25 17:18:19 -0700479 * a previous adapter view from {@link ConversationContainer}'s scrap views if one was generated
480 * earlier.
481 * <p>
Andy Huang46dfba62012-04-19 01:47:32 -0700482 * After measuring the height, this method also saves the height in the
483 * {@link ConversationOverlayItem} for later use in overlay positioning.
Andy Huang7bdc3752012-03-25 17:18:19 -0700484 *
Andy Huang46dfba62012-04-19 01:47:32 -0700485 * @param convItem adapter item with data to render and measure
Andy Huang7bdc3752012-03-25 17:18:19 -0700486 * @return height in dp of the rendered view
487 */
Andy Huang46dfba62012-04-19 01:47:32 -0700488 private int measureOverlayHeight(ConversationOverlayItem convItem) {
Andy Huang7bdc3752012-03-25 17:18:19 -0700489 final int type = convItem.getType();
490
491 final View convertView = mConversationContainer.getScrapView(type);
Andy Huang46dfba62012-04-19 01:47:32 -0700492 final View hostView = mAdapter.getView(convItem, convertView, mConversationContainer);
Andy Huang7bdc3752012-03-25 17:18:19 -0700493 if (convertView == null) {
494 mConversationContainer.addScrapView(type, hostView);
495 }
496
Andy Huang9875bb42012-04-04 20:36:21 -0700497 final int heightPx = mConversationContainer.measureOverlay(hostView);
Andy Huang7bdc3752012-03-25 17:18:19 -0700498 convItem.setHeight(heightPx);
Andy Huang9875bb42012-04-04 20:36:21 -0700499 convItem.markMeasurementValid();
Andy Huang7bdc3752012-03-25 17:18:19 -0700500
501 return (int) (heightPx / mDensity);
502 }
503
Andy Huang3233bff2012-03-20 19:38:45 -0700504 // BEGIN conversation header callbacks
Andy Huang5ff63742012-03-16 20:30:23 -0700505 @Override
506 public void onFoldersClicked() {
507 if (mChangeFoldersMenuItem == null) {
508 LogUtils.e(LOG_TAG, "unable to open 'change folders' dialog for a conversation");
509 return;
510 }
511 mActivity.onOptionsItemSelected(mChangeFoldersMenuItem);
512 }
513
514 @Override
515 public void onConversationViewHeaderHeightChange(int newHeight) {
516 // TODO: propagate the new height to the header's HTML spacer. This can happen when labels
517 // are added/removed
518 }
519
520 @Override
521 public String getSubjectRemainder(String subject) {
522 // TODO: hook this up to action bar
523 return subject;
524 }
Andy Huang3233bff2012-03-20 19:38:45 -0700525 // END conversation header callbacks
526
527 // START message header callbacks
528 @Override
Andy Huangc7543572012-04-03 15:34:29 -0700529 public void setMessageSpacerHeight(MessageHeaderItem item, int newSpacerHeightPx) {
530 mConversationContainer.invalidateSpacerGeometry();
531
532 // update message HTML spacer height
533 LogUtils.i(LOG_TAG, "setting HTML spacer h=%dpx", newSpacerHeightPx);
534 final int heightDp = (int) (newSpacerHeightPx / mDensity);
535 mWebView.loadUrl(String.format("javascript:setMessageHeaderSpacerHeight('%s', %d);",
536 mTemplates.getMessageDomId(item.message), heightDp));
Andy Huang3233bff2012-03-20 19:38:45 -0700537 }
538
539 @Override
Andy Huangc7543572012-04-03 15:34:29 -0700540 public void setMessageExpanded(MessageHeaderItem item, int newSpacerHeightPx) {
541 mConversationContainer.invalidateSpacerGeometry();
542
543 // show/hide the HTML message body and update the spacer height
544 LogUtils.i(LOG_TAG, "setting HTML spacer expanded=%s h=%dpx", item.isExpanded(),
545 newSpacerHeightPx);
546 final int heightDp = (int) (newSpacerHeightPx / mDensity);
547 mWebView.loadUrl(String.format("javascript:setMessageBodyVisible('%s', %s, %d);",
548 mTemplates.getMessageDomId(item.message), item.isExpanded(), heightDp));
Andy Huang3233bff2012-03-20 19:38:45 -0700549 }
550
551 @Override
552 public void showExternalResources(Message msg) {
553 mWebView.getSettings().setBlockNetworkImage(false);
554 mWebView.loadUrl("javascript:unblockImages('" + mTemplates.getMessageDomId(msg) + "');");
555 }
556 // END message header callbacks
Andy Huang5ff63742012-03-16 20:30:23 -0700557
Andy Huang46dfba62012-04-19 01:47:32 -0700558 @Override
559 public void onSuperCollapsedClick(SuperCollapsedBlockItem item) {
560 if (mCursor == null || !mViewsCreated) {
561 return;
562 }
563
564 mTempBodiesHtml = renderCollapsedHeaders(mCursor, item);
565 mWebView.loadUrl("javascript:replaceSuperCollapsedBlock(" + item.getStart() + ")");
566 }
567
Andy Huang5fbda022012-02-28 18:22:03 -0800568 private static class MessageLoader extends CursorLoader {
Paul Westbrook2d82d612012-03-07 09:21:14 -0800569 private boolean mDeliveredFirstResults = false;
Andy Huang5fbda022012-02-28 18:22:03 -0800570
571 public MessageLoader(Context c, Uri uri) {
572 super(c, uri, UIProvider.MESSAGE_PROJECTION, null, null, null);
573 }
574
575 @Override
576 public Cursor loadInBackground() {
577 return new MessageCursor(super.loadInBackground());
578
579 }
Paul Westbrook2d82d612012-03-07 09:21:14 -0800580
581 @Override
582 public void deliverResult(Cursor result) {
583 // We want to deliver these results, and then we want to make sure that any subsequent
584 // queries do not hit the network
585 super.deliverResult(result);
586
587 if (!mDeliveredFirstResults) {
588 mDeliveredFirstResults = true;
589 Uri uri = getUri();
590
591 // Create a ListParams that tells the provider to not hit the network
592 final ListParams listParams =
593 new ListParams(ListParams.NO_LIMIT, false /* useNetwork */);
594
595 // Build the new uri with this additional parameter
596 uri = uri.buildUpon().appendQueryParameter(
597 UIProvider.LIST_PARAMS_QUERY_PARAMETER, listParams.serialize()).build();
598 setUri(uri);
599 }
600 }
Andy Huang5fbda022012-02-28 18:22:03 -0800601 }
602
Andy Huangb5078b22012-03-05 19:52:29 -0800603 private static int[] parseInts(final String[] stringArray) {
604 final int len = stringArray.length;
605 final int[] ints = new int[len];
606 for (int i = 0; i < len; i++) {
607 ints[i] = Integer.parseInt(stringArray[i]);
608 }
609 return ints;
610 }
611
Andy Huang17a9cde2012-03-09 18:03:16 -0800612 private class ConversationWebViewClient extends WebViewClient {
613
614 @Override
615 public void onPageFinished(WebView view, String url) {
616 super.onPageFinished(view, url);
617
618 // TODO: save off individual message unread state (here, or in onLoadFinished?) so
619 // 'mark unread' restores the original unread state for each individual message
620
621 // mark as read upon open
Andy Huang51067132012-03-12 20:08:19 -0700622 if (!mConversation.read) {
623 mConversation.markRead(mContext, true /* read */);
Andy Huang397621b2012-03-14 20:52:39 -0700624 mConversation.read = true;
Andy Huang51067132012-03-12 20:08:19 -0700625 }
Andy Huang17a9cde2012-03-09 18:03:16 -0800626 }
627
Andy Huangaf5d4e02012-03-19 19:02:12 -0700628 @Override
629 public boolean shouldOverrideUrlLoading(WebView view, String url) {
Andy Huang46dfba62012-04-19 01:47:32 -0700630 final Activity activity = getActivity();
631 if (!mViewsCreated || activity == null) {
632 return false;
633 }
634
Andy Huangaf5d4e02012-03-19 19:02:12 -0700635 boolean result = false;
636 final Uri uri = Uri.parse(url);
637 Intent intent = new Intent(Intent.ACTION_VIEW, uri);
Andy Huang46dfba62012-04-19 01:47:32 -0700638 intent.putExtra(Browser.EXTRA_APPLICATION_ID, activity.getPackageName());
Andy Huangaf5d4e02012-03-19 19:02:12 -0700639 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
640
641 // FIXME: give provider a chance to customize url intents?
642 // Utils.addGoogleUriAccountIntentExtras(mContext, uri, mAccount, intent);
643
644 try {
Andy Huang46dfba62012-04-19 01:47:32 -0700645 activity.startActivity(intent);
Andy Huangaf5d4e02012-03-19 19:02:12 -0700646 result = true;
647 } catch (ActivityNotFoundException ex) {
648 // If no application can handle the URL, assume that the
649 // caller can handle it.
650 }
651
652 return result;
653 }
654
Andy Huang17a9cde2012-03-09 18:03:16 -0800655 }
656
Andy Huangf70fc402012-02-17 15:37:42 -0800657 /**
658 * NOTE: all public methods must be listed in the proguard flags so that they can be accessed
659 * via reflection and not stripped.
660 *
661 */
662 private class MailJsBridge {
663
664 @SuppressWarnings("unused")
Andy Huang7bdc3752012-03-25 17:18:19 -0700665 public void onWebContentGeometryChange(final String[] overlayBottomStrs) {
Andy Huang46dfba62012-04-19 01:47:32 -0700666 try {
667 mHandler.post(new Runnable() {
668 @Override
669 public void run() {
670 if (!mViewsCreated) {
671 LogUtils.d(LOG_TAG, "ignoring webContentGeometryChange because views" +
672 " are gone, %s", ConversationViewFragment.this);
673 return;
674 }
Andy Huang51067132012-03-12 20:08:19 -0700675
Andy Huang46dfba62012-04-19 01:47:32 -0700676 mConversationContainer.onGeometryChange(parseInts(overlayBottomStrs));
677 }
678 });
679 } catch (Throwable t) {
680 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onWebContentGeometryChange");
681 }
682 }
683
684 @SuppressWarnings("unused")
685 public String getTempMessageBodies() {
686 try {
687 if (!mViewsCreated) {
688 return "";
Andy Huangf70fc402012-02-17 15:37:42 -0800689 }
Andy Huang46dfba62012-04-19 01:47:32 -0700690
691 final String s = mTempBodiesHtml;
692 mTempBodiesHtml = null;
693 return s;
694 } catch (Throwable t) {
695 LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getTempMessageBodies");
696 return "";
697 }
Andy Huangf70fc402012-02-17 15:37:42 -0800698 }
699
700 }
701
Mindy Pereira9b875682012-02-15 18:10:54 -0800702}