ConversationViewFragment.java revision 5150f03723af8019169aeed8e406784da9c5f8f1
1/*
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;
22import android.app.LoaderManager;
23import android.content.ActivityNotFoundException;
24import android.content.Context;
25import android.content.CursorLoader;
26import android.content.Intent;
27import android.content.Loader;
28import android.database.Cursor;
29import android.net.Uri;
30import android.os.Bundle;
31import android.os.Handler;
32import android.provider.Browser;
33import android.view.LayoutInflater;
34import android.view.Menu;
35import android.view.MenuInflater;
36import android.view.MenuItem;
37import android.view.View;
38import android.view.ViewGroup;
39import android.webkit.ConsoleMessage;
40import android.webkit.WebChromeClient;
41import android.webkit.WebSettings;
42import android.webkit.WebView;
43import android.webkit.WebViewClient;
44
45import com.android.mail.R;
46import com.android.mail.browse.ConversationContainer;
47import com.android.mail.browse.ConversationOverlayItem;
48import com.android.mail.browse.ConversationViewAdapter;
49import com.android.mail.browse.ConversationViewAdapter.MessageFooterItem;
50import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem;
51import com.android.mail.browse.ConversationViewAdapter.SuperCollapsedBlockItem;
52import com.android.mail.browse.ConversationViewHeader;
53import com.android.mail.browse.ConversationWebView;
54import com.android.mail.browse.MessageCursor;
55import com.android.mail.browse.MessageFooterView;
56import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks;
57import com.android.mail.browse.SuperCollapsedBlock;
58import com.android.mail.providers.Account;
59import com.android.mail.providers.Address;
60import com.android.mail.providers.Conversation;
61import com.android.mail.providers.Folder;
62import com.android.mail.providers.ListParams;
63import com.android.mail.providers.Message;
64import com.android.mail.providers.Settings;
65import com.android.mail.providers.UIProvider;
66import com.android.mail.providers.UIProvider.AccountCapabilities;
67import com.android.mail.providers.UIProvider.FolderCapabilities;
68import com.android.mail.utils.LogUtils;
69import com.android.mail.utils.Utils;
70import com.google.common.collect.Lists;
71import com.google.common.collect.Maps;
72
73import java.util.List;
74import java.util.Map;
75
76
77/**
78 * The conversation view UI component.
79 */
80public final class ConversationViewFragment extends Fragment implements
81        LoaderManager.LoaderCallbacks<Cursor>,
82        ConversationViewHeader.ConversationViewHeaderCallbacks,
83        MessageHeaderViewCallbacks,
84        SuperCollapsedBlock.OnClickListener,
85        ConversationSender {
86
87    private static final String LOG_TAG = new LogUtils().getLogTag();
88    public static final String LAYOUT_TAG = "ConvLayout";
89
90    private static final int MESSAGE_LOADER_ID = 0;
91
92    private ControllableActivity mActivity;
93
94    private Context mContext;
95
96    private Conversation mConversation;
97
98    private ConversationContainer mConversationContainer;
99
100    private Account mAccount;
101
102    private ConversationWebView mWebView;
103
104    private HtmlConversationTemplates mTemplates;
105
106    private String mBaseUri;
107
108    private final Handler mHandler = new Handler();
109
110    private final MailJsBridge mJsBridge = new MailJsBridge();
111
112    private final WebViewClient mWebViewClient = new ConversationWebViewClient();
113
114    private ConversationViewAdapter mAdapter;
115    private MessageCursor mCursor;
116
117    private boolean mViewsCreated;
118
119    private MenuItem mChangeFoldersMenuItem;
120
121    private float mDensity;
122
123    /**
124     * Folder is used to help determine valid menu actions for this conversation.
125     */
126    private Folder mFolder;
127
128    private AbstractActivityController mConversationRouter;
129
130    private final Map<String, Address> mAddressCache = Maps.newHashMap();
131
132    /**
133     * Temporary string containing the message bodies of the messages within a super-collapsed
134     * block, for one-time use during block expansion. We cannot easily pass the body HTML
135     * into JS without problematic escaping, so hold onto it momentarily and signal JS to fetch it
136     * using {@link MailJsBridge}.
137     */
138    private String mTempBodiesHtml;
139
140    private boolean mUserVisible;
141
142    private int  mMaxAutoLoadMessages;
143
144    private boolean mDeferredConversationLoad;
145
146    private static final String ARG_ACCOUNT = "account";
147    public static final String ARG_CONVERSATION = "conversation";
148    private static final String ARG_FOLDER = "folder";
149
150    /**
151     * Constructor needs to be public to handle orientation changes and activity lifecycle events.
152     */
153    public ConversationViewFragment() {
154        super();
155    }
156
157    /**
158     * Creates a new instance of {@link ConversationViewFragment}, initialized
159     * to display a conversation.
160     */
161    public static ConversationViewFragment newInstance(Account account,
162            Conversation conversation, Folder folder) {
163       ConversationViewFragment f = new ConversationViewFragment();
164       Bundle args = new Bundle();
165       args.putParcelable(ARG_ACCOUNT, account);
166       args.putParcelable(ARG_CONVERSATION, conversation);
167       args.putParcelable(ARG_FOLDER, folder);
168       f.setArguments(args);
169       return f;
170    }
171
172    /**
173     * Creates a new instance of {@link ConversationViewFragment}, initialized
174     * to display a conversation with other parameters inherited/copied from an existing bundle,
175     * typically one created using {@link #makeBasicArgs}.
176     */
177    public static ConversationViewFragment newInstance(Bundle existingArgs,
178            Conversation conversation) {
179        ConversationViewFragment f = new ConversationViewFragment();
180        Bundle args = new Bundle(existingArgs);
181        args.putParcelable(ARG_CONVERSATION, conversation);
182        f.setArguments(args);
183        return f;
184    }
185
186    public static Bundle makeBasicArgs(Account account, Folder folder) {
187        Bundle args = new Bundle();
188        args.putParcelable(ARG_ACCOUNT, account);
189        args.putParcelable(ARG_FOLDER, folder);
190        return args;
191    }
192
193    @Override
194    public void onActivityCreated(Bundle savedInstanceState) {
195        LogUtils.d(LOG_TAG, "IN CVF.onActivityCreated, this=%s subj=%s", this,
196                mConversation.subject);
197        super.onActivityCreated(savedInstanceState);
198        // Strictly speaking, we get back an android.app.Activity from getActivity. However, the
199        // only activity creating a ConversationListContext is a MailActivity which is of type
200        // ControllableActivity, so this cast should be safe. If this cast fails, some other
201        // activity is creating ConversationListFragments. This activity must be of type
202        // ControllableActivity.
203        final Activity activity = getActivity();
204        if (!(activity instanceof ControllableActivity)) {
205            LogUtils.wtf(LOG_TAG, "ConversationViewFragment expects only a ControllableActivity to"
206                    + "create it. Cannot proceed.");
207        }
208        mActivity = (ControllableActivity) activity;
209        mContext = mActivity.getApplicationContext();
210        if (mActivity.isFinishing()) {
211            // Activity is finishing, just bail.
212            return;
213        }
214        mTemplates = new HtmlConversationTemplates(mContext);
215
216        mAdapter = new ConversationViewAdapter(mActivity.getActivityContext(), mAccount,
217                getLoaderManager(), this, this, this, mAddressCache);
218        mConversationContainer.setOverlayAdapter(mAdapter);
219
220        mDensity = getResources().getDisplayMetrics().density;
221
222        mMaxAutoLoadMessages = getResources().getInteger(R.integer.max_auto_load_messages);
223
224        showConversation();
225    }
226
227    @Override
228    public void onCreate(Bundle savedState) {
229        LogUtils.d(LOG_TAG, "onCreate in ConversationViewFragment (this=%s)", this);
230        super.onCreate(savedState);
231
232        Bundle args = getArguments();
233        mAccount = args.getParcelable(ARG_ACCOUNT);
234        mConversation = args.getParcelable(ARG_CONVERSATION);
235        mFolder = args.getParcelable(ARG_FOLDER);
236        mBaseUri = "x-thread://" + mAccount.name + "/" + mConversation.id;
237
238        // not really, we just want to get a crack to store a reference to the change_folders item
239        setHasOptionsMenu(true);
240    }
241
242    @Override
243    public View onCreateView(LayoutInflater inflater,
244            ViewGroup container, Bundle savedInstanceState) {
245        View rootView = inflater.inflate(R.layout.conversation_view, container, false);
246        mConversationContainer = (ConversationContainer) rootView
247                .findViewById(R.id.conversation_container);
248        mWebView = (ConversationWebView) mConversationContainer.findViewById(R.id.webview);
249
250        mWebView.addJavascriptInterface(mJsBridge, "mail");
251        mWebView.setWebViewClient(mWebViewClient);
252        mWebView.setWebChromeClient(new WebChromeClient() {
253            @Override
254            public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
255                LogUtils.i(LOG_TAG, "JS: %s (%s:%d)", consoleMessage.message(),
256                        consoleMessage.sourceId(), consoleMessage.lineNumber());
257                return true;
258            }
259        });
260
261        final WebSettings settings = mWebView.getSettings();
262
263        settings.setJavaScriptEnabled(true);
264        settings.setUseWideViewPort(true);
265
266        settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL);
267
268        settings.setSupportZoom(true);
269        settings.setBuiltInZoomControls(true);
270        settings.setDisplayZoomControls(false);
271
272        mViewsCreated = true;
273
274        return rootView;
275    }
276
277    @Override
278    public void onDestroyView() {
279        super.onDestroyView();
280        mConversationContainer.setOverlayAdapter(null);
281        mAdapter = null;
282        mViewsCreated = false;
283    }
284
285    @Override
286    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
287        super.onCreateOptionsMenu(menu, inflater);
288
289        mChangeFoldersMenuItem = menu.findItem(R.id.change_folders);
290    }
291
292    @Override
293    public void onPrepareOptionsMenu(Menu menu) {
294        super.onPrepareOptionsMenu(menu);
295        boolean showMarkImportant = !mConversation.isImportant();
296        Utils.setMenuItemVisibility(
297                menu,
298                R.id.mark_important,
299                showMarkImportant
300                        && mAccount
301                                .supportsCapability(UIProvider.AccountCapabilities.MARK_IMPORTANT));
302        Utils.setMenuItemVisibility(
303                menu,
304                R.id.mark_not_important,
305                !showMarkImportant
306                        && mAccount
307                                .supportsCapability(UIProvider.AccountCapabilities.MARK_IMPORTANT));
308        // TODO(mindyp) show/ hide spam and mute based on conversation
309        // properties to be added.
310        Utils.setMenuItemVisibility(menu, R.id.y_button,
311                mAccount.supportsCapability(AccountCapabilities.ARCHIVE) && mFolder != null
312                        && mFolder.supportsCapability(FolderCapabilities.ARCHIVE));
313        Utils.setMenuItemVisibility(menu, R.id.report_spam,
314                mAccount.supportsCapability(AccountCapabilities.REPORT_SPAM) && mFolder != null
315                        && mFolder.supportsCapability(FolderCapabilities.REPORT_SPAM)
316                        && !mConversation.spam);
317        Utils.setMenuItemVisibility(
318                menu,
319                R.id.mute,
320                mAccount.supportsCapability(AccountCapabilities.MUTE) && mFolder != null
321                        && mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE)
322                        && !mConversation.muted);
323    }
324
325    /**
326     * {@link #setUserVisibleHint(boolean)} only works on API >= 15, so implement our own for
327     * reliability on older platforms.
328     */
329    public void setExtraUserVisibleHint(boolean isVisibleToUser) {
330        LogUtils.v(LOG_TAG, "in CVF.setHint, val=%s (%s)", isVisibleToUser, this);
331
332        if (mUserVisible != isVisibleToUser) {
333            mUserVisible = isVisibleToUser;
334
335            if (isVisibleToUser && mViewsCreated) {
336
337                if (mCursor == null && mDeferredConversationLoad) {
338                    // load
339                    LogUtils.v(LOG_TAG, "Fragment is now user-visible, showing conversation: %s",
340                            mConversation.uri);
341                    showConversation();
342                    mDeferredConversationLoad = false;
343                } else {
344                    onConversationSeen();
345                }
346
347            }
348        }
349    }
350
351    /**
352     * Handles a request to show a new conversation list, either from a search query or for viewing
353     * a folder. This will initiate a data load, and hence must be called on the UI thread.
354     */
355    private void showConversation() {
356        if (!mUserVisible && mConversation.numMessages > mMaxAutoLoadMessages) {
357            LogUtils.v(LOG_TAG, "Fragment not user-visible, not showing conversation: %s",
358                    mConversation.uri);
359            mDeferredConversationLoad = true;
360            return;
361        }
362        LogUtils.v(LOG_TAG,
363                "Fragment is short or user-visible, immediately rendering conversation: %s",
364                mConversation.uri);
365        getLoaderManager().initLoader(MESSAGE_LOADER_ID, Bundle.EMPTY, this);
366    }
367
368    public Conversation getConversation() {
369        return mConversation;
370    }
371
372    @Override
373    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
374        return new MessageLoader(mContext, mConversation.messageListUri, this);
375    }
376
377    @Override
378    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
379        MessageCursor messageCursor = (MessageCursor) data;
380
381        // ignore truly duplicate results
382        // this can happen when restoring after rotation
383        if (mCursor == messageCursor) {
384            return;
385        }
386
387        // TODO: handle Gmail loading states (like LOADING and ERROR)
388        if (messageCursor.getCount() == 0) {
389            if (mCursor != null) {
390                // TODO: need to exit this view- conversation may have been deleted, or for
391                // whatever reason is now invalid
392            } else {
393                // ignore zero-sized cursors during initial load
394            }
395            return;
396        }
397
398        // TODO: if this is not user-visible, delay render until user-visible fragment is done.
399        // This is needed in addition to the showConversation() delay to speed up rotation and
400        // restoration.
401
402        renderConversation(messageCursor);
403    }
404
405    @Override
406    public void onLoaderReset(Loader<Cursor> loader) {
407        mCursor = null;
408        // TODO: null out all Message.mMessageCursor references
409    }
410
411    private void renderConversation(MessageCursor messageCursor) {
412        mWebView.loadDataWithBaseURL(mBaseUri, renderMessageBodies(messageCursor), "text/html",
413                "utf-8", null);
414        mCursor = messageCursor;
415    }
416
417    private void updateConversation(MessageCursor messageCursor) {
418        // TODO: handle server-side conversation updates
419        // for simple things like header data changes, just re-render the affected headers
420        // if a new message is present, save off the pending cursor and show a notification to
421        // re-render
422
423        mCursor = messageCursor;
424    }
425
426    /**
427     * Populate the adapter with overlay views (message headers, super-collapsed blocks, a
428     * conversation header), and return an HTML document with spacer divs inserted for all overlays.
429     *
430     */
431    private String renderMessageBodies(MessageCursor messageCursor) {
432        int pos = -1;
433
434        LogUtils.d(LOG_TAG, "IN renderMessageBodies, fragment=%s subj=%s", this,
435                mConversation.subject);
436        boolean allowNetworkImages = false;
437
438        // TODO: re-use any existing adapter item state (expanded, details expanded, show pics)
439        final Settings settings = mActivity.getSettings();
440        if (settings != null) {
441            mAdapter.setDefaultReplyAll(settings.replyBehavior ==
442                    UIProvider.DefaultReplyBehavior.REPLY_ALL);
443        }
444        // Walk through the cursor and build up an overlay adapter as you go.
445        // Each overlay has an entry in the adapter for easy scroll handling in the container.
446        // Items are not necessarily 1:1 in cursor and adapter because of super-collapsed blocks.
447        // When adding adapter items, also add their heights to help the container later determine
448        // overlay dimensions.
449
450        mAdapter.clear();
451
452        // We don't need to kick off attachment loaders during this first measurement phase,
453        // so disable them temporarily.
454        MessageFooterView.enableAttachmentLoaders(false);
455
456        // N.B. the units of height for spacers are actually dp and not px because WebView assumes
457        // a pixel is an mdpi pixel, unless you set device-dpi.
458
459        // add a single conversation header item
460        final int convHeaderPos = mAdapter.addConversationHeader(mConversation);
461        final int convHeaderDp = measureOverlayHeight(convHeaderPos);
462
463        mTemplates.startConversation(convHeaderDp);
464
465        int collapsedStart = -1;
466        Message prevCollapsedMsg = null;
467        boolean prevSafeForImages = false;
468
469        while (messageCursor.moveToPosition(++pos)) {
470            final Message msg = messageCursor.getMessage();
471
472            // TODO: save/restore 'show pics' state
473            final boolean safeForImages = msg.alwaysShowImages /* || savedStateSaysSafe */;
474            allowNetworkImages |= safeForImages;
475
476            final boolean expanded = !msg.read || msg.starred || messageCursor.isLast();
477
478            if (!expanded) {
479                // contribute to a super-collapsed block that will be emitted just before the next
480                // expanded header
481                if (collapsedStart < 0) {
482                    collapsedStart = pos;
483                }
484                prevCollapsedMsg = msg;
485                prevSafeForImages = safeForImages;
486                continue;
487            }
488
489            // resolve any deferred decisions on previous collapsed items
490            if (collapsedStart >= 0) {
491                if (pos - collapsedStart == 1) {
492                    // special-case for a single collapsed message: no need to super-collapse it
493                    renderMessage(prevCollapsedMsg, false /* expanded */,
494                            prevSafeForImages);
495                } else {
496                    renderSuperCollapsedBlock(collapsedStart, pos - 1);
497                }
498                prevCollapsedMsg = null;
499                collapsedStart = -1;
500            }
501
502            renderMessage(msg, expanded, safeForImages);
503        }
504
505        // Re-enable attachment loaders
506        MessageFooterView.enableAttachmentLoaders(true);
507
508        mWebView.getSettings().setBlockNetworkImage(!allowNetworkImages);
509
510        return mTemplates.endConversation(mBaseUri, 320);
511    }
512
513    private void renderSuperCollapsedBlock(int start, int end) {
514        final int blockPos = mAdapter.addSuperCollapsedBlock(start, end);
515        final int blockDp = measureOverlayHeight(blockPos);
516        mTemplates.appendSuperCollapsedHtml(start, blockDp);
517    }
518
519    private void renderMessage(Message msg, boolean expanded, boolean safeForImages) {
520        final int headerPos = mAdapter.addMessageHeader(msg, expanded);
521        final MessageHeaderItem headerItem = (MessageHeaderItem) mAdapter.getItem(headerPos);
522
523        final int footerPos = mAdapter.addMessageFooter(headerItem);
524
525        // Measure item header and footer heights to allocate spacers in HTML
526        // But since the views themselves don't exist yet, render each item temporarily into
527        // a host view for measurement.
528        final int headerDp = measureOverlayHeight(headerPos);
529        final int footerDp = measureOverlayHeight(footerPos);
530
531        mTemplates.appendMessageHtml(msg, expanded, safeForImages, 1.0f, headerDp,
532                footerDp);
533    }
534
535    private String renderCollapsedHeaders(MessageCursor cursor,
536            SuperCollapsedBlockItem blockToReplace) {
537        final List<ConversationOverlayItem> replacements = Lists.newArrayList();
538
539        mTemplates.reset();
540
541        for (int i = blockToReplace.getStart(), end = blockToReplace.getEnd(); i <= end; i++) {
542            cursor.moveToPosition(i);
543            final Message msg = cursor.getMessage();
544            final MessageHeaderItem header = mAdapter.newMessageHeaderItem(msg,
545                    false /* expanded */);
546            final MessageFooterItem footer = mAdapter.newMessageFooterItem(header);
547
548            final int headerDp = measureOverlayHeight(header);
549            final int footerDp = measureOverlayHeight(footer);
550
551            mTemplates.appendMessageHtml(msg, false /* expanded */, msg.alwaysShowImages, 1.0f,
552                    headerDp, footerDp);
553            replacements.add(header);
554            replacements.add(footer);
555        }
556
557        mAdapter.replaceSuperCollapsedBlock(blockToReplace, replacements);
558
559        return mTemplates.emit();
560    }
561
562    private int measureOverlayHeight(int position) {
563        return measureOverlayHeight(mAdapter.getItem(position));
564    }
565
566    /**
567     * Measure the height of an adapter view by rendering and adapter item into a temporary
568     * host view, and asking the view to immediately measure itself. This method will reuse
569     * a previous adapter view from {@link ConversationContainer}'s scrap views if one was generated
570     * earlier.
571     * <p>
572     * After measuring the height, this method also saves the height in the
573     * {@link ConversationOverlayItem} for later use in overlay positioning.
574     *
575     * @param convItem adapter item with data to render and measure
576     * @return height in dp of the rendered view
577     */
578    private int measureOverlayHeight(ConversationOverlayItem convItem) {
579        final int type = convItem.getType();
580
581        final View convertView = mConversationContainer.getScrapView(type);
582        final View hostView = mAdapter.getView(convItem, convertView, mConversationContainer);
583        if (convertView == null) {
584            mConversationContainer.addScrapView(type, hostView);
585        }
586
587        final int heightPx = mConversationContainer.measureOverlay(hostView);
588        convItem.setHeight(heightPx);
589        convItem.markMeasurementValid();
590
591        return (int) (heightPx / mDensity);
592    }
593
594    private void onConversationSeen() {
595        // mark as read upon open
596        if (!mConversation.read) {
597            mConversationRouter.sendConversationRead(
598                    AbstractActivityController.TAG_CONVERSATION_LIST, mConversation, true,
599                    false /*local*/);
600            mConversation.read = true;
601        }
602
603        ControllableActivity activity = (ControllableActivity) getActivity();
604        if (activity != null) {
605            activity.onConversationSeen(mConversation);
606        }
607    }
608
609    // BEGIN conversation header callbacks
610    @Override
611    public void onFoldersClicked() {
612        if (mChangeFoldersMenuItem == null) {
613            LogUtils.e(LOG_TAG, "unable to open 'change folders' dialog for a conversation");
614            return;
615        }
616        mActivity.onOptionsItemSelected(mChangeFoldersMenuItem);
617    }
618
619    @Override
620    public void onConversationViewHeaderHeightChange(int newHeight) {
621        // TODO: propagate the new height to the header's HTML spacer. This can happen when labels
622        // are added/removed
623    }
624
625    @Override
626    public String getSubjectRemainder(String subject) {
627        // TODO: hook this up to action bar
628        return subject;
629    }
630    // END conversation header callbacks
631
632    // START message header callbacks
633    @Override
634    public void setMessageSpacerHeight(MessageHeaderItem item, int newSpacerHeightPx) {
635        mConversationContainer.invalidateSpacerGeometry();
636
637        // update message HTML spacer height
638        LogUtils.i(LAYOUT_TAG, "setting HTML spacer h=%dpx", newSpacerHeightPx);
639        final int heightDp = (int) (newSpacerHeightPx / mDensity);
640        mWebView.loadUrl(String.format("javascript:setMessageHeaderSpacerHeight('%s', %d);",
641                mTemplates.getMessageDomId(item.message), heightDp));
642    }
643
644    @Override
645    public void setMessageExpanded(MessageHeaderItem item, int newSpacerHeightPx) {
646        mConversationContainer.invalidateSpacerGeometry();
647
648        // show/hide the HTML message body and update the spacer height
649        LogUtils.i(LAYOUT_TAG, "setting HTML spacer expanded=%s h=%dpx", item.isExpanded(),
650                newSpacerHeightPx);
651        final int heightDp = (int) (newSpacerHeightPx / mDensity);
652        mWebView.loadUrl(String.format("javascript:setMessageBodyVisible('%s', %s, %d);",
653                mTemplates.getMessageDomId(item.message), item.isExpanded(), heightDp));
654    }
655
656    @Override
657    public void showExternalResources(Message msg) {
658        mWebView.getSettings().setBlockNetworkImage(false);
659        mWebView.loadUrl("javascript:unblockImages('" + mTemplates.getMessageDomId(msg) + "');");
660    }
661    // END message header callbacks
662
663    @Override
664    public void onSuperCollapsedClick(SuperCollapsedBlockItem item) {
665        if (mCursor == null || !mViewsCreated) {
666            return;
667        }
668
669        mTempBodiesHtml = renderCollapsedHeaders(mCursor, item);
670        mWebView.loadUrl("javascript:replaceSuperCollapsedBlock(" + item.getStart() + ")");
671    }
672
673    private static class MessageLoader extends CursorLoader {
674        private boolean mDeliveredFirstResults = false;
675        private final ConversationViewFragment mFragment;
676
677        public MessageLoader(Context c, Uri uri, ConversationViewFragment fragment) {
678            super(c, uri, UIProvider.MESSAGE_PROJECTION, null, null, null);
679            mFragment = fragment;
680        }
681
682        @Override
683        public Cursor loadInBackground() {
684            return new MessageCursor(super.loadInBackground(), mFragment);
685
686        }
687
688        @Override
689        public void deliverResult(Cursor result) {
690            // We want to deliver these results, and then we want to make sure that any subsequent
691            // queries do not hit the network
692            super.deliverResult(result);
693
694            if (!mDeliveredFirstResults) {
695                mDeliveredFirstResults = true;
696                Uri uri = getUri();
697
698                // Create a ListParams that tells the provider to not hit the network
699                final ListParams listParams =
700                        new ListParams(ListParams.NO_LIMIT, false /* useNetwork */);
701
702                // Build the new uri with this additional parameter
703                uri = uri.buildUpon().appendQueryParameter(
704                        UIProvider.LIST_PARAMS_QUERY_PARAMETER, listParams.serialize()).build();
705                setUri(uri);
706            }
707        }
708    }
709
710    private static int[] parseInts(final String[] stringArray) {
711        final int len = stringArray.length;
712        final int[] ints = new int[len];
713        for (int i = 0; i < len; i++) {
714            ints[i] = Integer.parseInt(stringArray[i]);
715        }
716        return ints;
717    }
718
719    private class ConversationWebViewClient extends WebViewClient {
720
721        @Override
722        public void onPageFinished(WebView view, String url) {
723            LogUtils.i(LOG_TAG, "IN CVF.onPageFinished, url=%s fragment=%s", url,
724                    ConversationViewFragment.this);
725
726            super.onPageFinished(view, url);
727
728            // TODO: save off individual message unread state (here, or in onLoadFinished?) so
729            // 'mark unread' restores the original unread state for each individual message
730
731            if (mUserVisible) {
732                onConversationSeen();
733            }
734        }
735
736        @Override
737        public boolean shouldOverrideUrlLoading(WebView view, String url) {
738            final Activity activity = getActivity();
739            if (!mViewsCreated || activity == null) {
740                return false;
741            }
742
743            boolean result = false;
744            final Uri uri = Uri.parse(url);
745            Intent intent = new Intent(Intent.ACTION_VIEW, uri);
746            intent.putExtra(Browser.EXTRA_APPLICATION_ID, activity.getPackageName());
747            intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
748
749            // FIXME: give provider a chance to customize url intents?
750            // Utils.addGoogleUriAccountIntentExtras(mContext, uri, mAccount, intent);
751
752            try {
753                activity.startActivity(intent);
754                result = true;
755            } catch (ActivityNotFoundException ex) {
756                // If no application can handle the URL, assume that the
757                // caller can handle it.
758            }
759
760            return result;
761        }
762
763    }
764
765    /**
766     * NOTE: all public methods must be listed in the proguard flags so that they can be accessed
767     * via reflection and not stripped.
768     *
769     */
770    private class MailJsBridge {
771
772        @SuppressWarnings("unused")
773        public void onWebContentGeometryChange(final String[] overlayBottomStrs) {
774            try {
775                mHandler.post(new Runnable() {
776                    @Override
777                    public void run() {
778                        if (!mViewsCreated) {
779                            LogUtils.d(LOG_TAG, "ignoring webContentGeometryChange because views" +
780                                    " are gone, %s", ConversationViewFragment.this);
781                            return;
782                        }
783
784                        mConversationContainer.onGeometryChange(parseInts(overlayBottomStrs));
785                    }
786                });
787            } catch (Throwable t) {
788                LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onWebContentGeometryChange");
789            }
790        }
791
792        @SuppressWarnings("unused")
793        public String getTempMessageBodies() {
794            try {
795                if (!mViewsCreated) {
796                    return "";
797                }
798
799                final String s = mTempBodiesHtml;
800                mTempBodiesHtml = null;
801                return s;
802            } catch (Throwable t) {
803                LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getTempMessageBodies");
804                return "";
805            }
806        }
807
808    }
809
810    // Is the conversation starred?
811    public boolean isConversationStarred() {
812        int pos = -1;
813        while (mCursor.moveToPosition(++pos)) {
814            Message m = mCursor.getMessage();
815            if (m.starred) {
816                return true;
817            }
818        }
819        return false;
820    }
821
822    @Override
823    public void setConversationRouter(AbstractActivityController conversationRouter) {
824        mConversationRouter = conversationRouter;
825    }
826
827    public AbstractActivityController getConversationRouter() {
828        return mConversationRouter;
829    }
830
831}
832