ConversationViewFragment.java revision 28b7aee7fa1f7d096d33fc823a88a64f7a3fa79d
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.database.DataSetObservable;
30import android.database.DataSetObserver;
31import android.net.Uri;
32import android.os.AsyncTask;
33import android.os.Bundle;
34import android.os.Handler;
35import android.provider.Browser;
36import android.text.TextUtils;
37import android.view.LayoutInflater;
38import android.view.Menu;
39import android.view.MenuInflater;
40import android.view.MenuItem;
41import android.view.View;
42import android.view.ViewGroup;
43import android.webkit.ConsoleMessage;
44import android.webkit.CookieManager;
45import android.webkit.CookieSyncManager;
46import android.webkit.WebChromeClient;
47import android.webkit.WebSettings;
48import android.webkit.WebView;
49import android.webkit.WebViewClient;
50import android.widget.TextView;
51
52import com.android.mail.ContactInfo;
53import com.android.mail.ContactInfoSource;
54import com.android.mail.FormattedDateBuilder;
55import com.android.mail.R;
56import com.android.mail.SenderInfoLoader;
57import com.android.mail.browse.ConversationContainer;
58import com.android.mail.browse.ConversationOverlayItem;
59import com.android.mail.browse.ConversationViewAdapter;
60import com.android.mail.browse.ConversationViewAdapter.ConversationAccountController;
61import com.android.mail.browse.ConversationViewAdapter.MessageFooterItem;
62import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem;
63import com.android.mail.browse.ConversationViewAdapter.SuperCollapsedBlockItem;
64import com.android.mail.browse.ConversationViewHeader;
65import com.android.mail.browse.ConversationWebView;
66import com.android.mail.browse.MessageCursor;
67import com.android.mail.browse.MessageCursor.ConversationController;
68import com.android.mail.browse.MessageCursor.ConversationMessage;
69import com.android.mail.browse.MessageHeaderView;
70import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks;
71import com.android.mail.browse.SuperCollapsedBlock;
72import com.android.mail.browse.WebViewContextMenu;
73import com.android.mail.providers.Account;
74import com.android.mail.providers.AccountObserver;
75import com.android.mail.providers.Address;
76import com.android.mail.providers.Conversation;
77import com.android.mail.providers.Folder;
78import com.android.mail.providers.ListParams;
79import com.android.mail.providers.Message;
80import com.android.mail.providers.UIProvider;
81import com.android.mail.providers.UIProvider.AccountCapabilities;
82import com.android.mail.providers.UIProvider.FolderCapabilities;
83import com.android.mail.ui.ConversationViewState.ExpansionState;
84import com.android.mail.utils.LogTag;
85import com.android.mail.utils.LogUtils;
86import com.android.mail.utils.Utils;
87import com.google.common.collect.ImmutableMap;
88import com.google.common.collect.Lists;
89import com.google.common.collect.Maps;
90import com.google.common.collect.Sets;
91
92import java.util.Arrays;
93import java.util.List;
94import java.util.Map;
95import java.util.Set;
96
97
98/**
99 * The conversation view UI component.
100 */
101public final class ConversationViewFragment extends Fragment implements
102        ConversationViewHeader.ConversationViewHeaderCallbacks,
103        MessageHeaderViewCallbacks,
104        SuperCollapsedBlock.OnClickListener,
105        ConversationController,
106        ConversationAccountController {
107
108    private static final String LOG_TAG = LogTag.getLogTag();
109    public static final String LAYOUT_TAG = "ConvLayout";
110
111    private static final int MESSAGE_LOADER_ID = 0;
112    private static final int CONTACT_LOADER_ID = 1;
113
114    private ControllableActivity mActivity;
115
116    private Context mContext;
117
118    private Conversation mConversation;
119
120    private ConversationContainer mConversationContainer;
121
122    private Account mAccount;
123
124    private ConversationWebView mWebView;
125
126    private View mNewMessageBar;
127
128    private HtmlConversationTemplates mTemplates;
129
130    private String mBaseUri;
131
132    private final Handler mHandler = new Handler();
133
134    private final MailJsBridge mJsBridge = new MailJsBridge();
135
136    private final WebViewClient mWebViewClient = new ConversationWebViewClient();
137
138    private ConversationViewAdapter mAdapter;
139    private MessageCursor mCursor;
140
141    private boolean mViewsCreated;
142
143    private MenuItem mChangeFoldersMenuItem;
144
145    /**
146     * Folder is used to help determine valid menu actions for this conversation.
147     */
148    private Folder mFolder;
149
150    private final Map<String, Address> mAddressCache = Maps.newHashMap();
151
152    /**
153     * Temporary string containing the message bodies of the messages within a super-collapsed
154     * block, for one-time use during block expansion. We cannot easily pass the body HTML
155     * into JS without problematic escaping, so hold onto it momentarily and signal JS to fetch it
156     * using {@link MailJsBridge}.
157     */
158    private String mTempBodiesHtml;
159
160    private boolean mUserVisible;
161
162    private int  mMaxAutoLoadMessages;
163
164    private boolean mDeferredConversationLoad;
165
166    /**
167     * Handles a deferred 'mark read' operation, necessary when the conversation view has finished
168     * loading before the conversation cursor. Normally null unless this situation occurs.
169     * When finally able to 'mark read', this observer will also be unregistered and cleaned up.
170     */
171    private MarkReadObserver mMarkReadObserver;
172
173    /**
174     * Parcelable state of the conversation view. Can safely be used without null checking any time
175     * after {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}.
176     */
177    private ConversationViewState mViewState;
178
179    private final MessageLoaderCallbacks mMessageLoaderCallbacks = new MessageLoaderCallbacks();
180    private final ContactLoaderCallbacks mContactLoaderCallbacks = new ContactLoaderCallbacks();
181
182    private final AccountObserver mAccountObserver = new AccountObserver() {
183        @Override
184        public void onChanged(Account newAccount) {
185            mAccount = newAccount;
186
187            // settings may have been updated; refresh views that are known to depend on settings
188            mConversationContainer.getSnapHeader().onAccountChanged();
189            mAdapter.notifyDataSetChanged();
190        }
191    };
192
193    private static final String ARG_ACCOUNT = "account";
194    public static final String ARG_CONVERSATION = "conversation";
195    private static final String ARG_FOLDER = "folder";
196    private static final String BUNDLE_VIEW_STATE = "viewstate";
197
198    private static final boolean DEBUG_DUMP_CONVERSATION_HTML = false;
199    private static final boolean DISABLE_OFFSCREEN_LOADING = false;
200
201    /**
202     * Constructor needs to be public to handle orientation changes and activity lifecycle events.
203     */
204    public ConversationViewFragment() {
205        super();
206    }
207
208    /**
209     * Creates a new instance of {@link ConversationViewFragment}, initialized
210     * to display a conversation with other parameters inherited/copied from an existing bundle,
211     * typically one created using {@link #makeBasicArgs}.
212     */
213    public static ConversationViewFragment newInstance(Bundle existingArgs,
214            Conversation conversation) {
215        ConversationViewFragment f = new ConversationViewFragment();
216        Bundle args = new Bundle(existingArgs);
217        args.putParcelable(ARG_CONVERSATION, conversation);
218        f.setArguments(args);
219        return f;
220    }
221
222    public static Bundle makeBasicArgs(Account account, Folder folder) {
223        Bundle args = new Bundle();
224        args.putParcelable(ARG_ACCOUNT, account);
225        args.putParcelable(ARG_FOLDER, folder);
226        return args;
227    }
228
229    @Override
230    public void onActivityCreated(Bundle savedInstanceState) {
231        LogUtils.d(LOG_TAG, "IN CVF.onActivityCreated, this=%s subj=%s", this,
232                mConversation.subject);
233        super.onActivityCreated(savedInstanceState);
234        // Strictly speaking, we get back an android.app.Activity from getActivity. However, the
235        // only activity creating a ConversationListContext is a MailActivity which is of type
236        // ControllableActivity, so this cast should be safe. If this cast fails, some other
237        // activity is creating ConversationListFragments. This activity must be of type
238        // ControllableActivity.
239        final Activity activity = getActivity();
240        if (!(activity instanceof ControllableActivity)) {
241            LogUtils.wtf(LOG_TAG, "ConversationViewFragment expects only a ControllableActivity to"
242                    + "create it. Cannot proceed.");
243        }
244        mActivity = (ControllableActivity) activity;
245        mContext = mActivity.getApplicationContext();
246        if (mActivity.isFinishing()) {
247            // Activity is finishing, just bail.
248            return;
249        }
250        mTemplates = new HtmlConversationTemplates(mContext);
251        mAccount = mAccountObserver.initialize(mActivity.getAccountController());
252
253        final FormattedDateBuilder dateBuilder = new FormattedDateBuilder(mContext);
254
255        mAdapter = new ConversationViewAdapter(mActivity.getActivityContext(), this,
256                getLoaderManager(), this, mContactLoaderCallbacks, this, this, mAddressCache,
257                dateBuilder);
258        mConversationContainer.setOverlayAdapter(mAdapter);
259
260        // set up snap header (the adapter usually does this with the other ones)
261        final MessageHeaderView snapHeader = mConversationContainer.getSnapHeader();
262        snapHeader.initialize(dateBuilder, this, mAddressCache);
263        snapHeader.setCallbacks(this);
264        snapHeader.setContactInfoSource(mContactLoaderCallbacks);
265
266        mMaxAutoLoadMessages = getResources().getInteger(R.integer.max_auto_load_messages);
267
268        mWebView.setOnCreateContextMenuListener(new WebViewContextMenu(activity));
269
270        showConversation();
271
272        if (mConversation.conversationBaseUri != null &&
273                !TextUtils.isEmpty(mConversation.conversationCookie)) {
274            // Set the cookie for this base url
275            new SetCookieTask(mConversation.conversationBaseUri.toString(),
276                    mConversation.conversationCookie).execute();
277        }
278    }
279
280    @Override
281    public void onCreate(Bundle savedState) {
282        LogUtils.d(LOG_TAG, "onCreate in ConversationViewFragment (this=%s)", this);
283        super.onCreate(savedState);
284
285        final Bundle args = getArguments();
286        mAccount = args.getParcelable(ARG_ACCOUNT);
287        mConversation = args.getParcelable(ARG_CONVERSATION);
288        mFolder = args.getParcelable(ARG_FOLDER);
289        // Since the uri specified in the conversation base uri may not be unique, we specify a
290        // base uri that us guaranteed to be unique for this conversation.
291        mBaseUri = "x-thread://" + mAccount.name + "/" + mConversation.id;
292
293        // Not really, we just want to get a crack to store a reference to the change_folder item
294        setHasOptionsMenu(true);
295    }
296
297    @Override
298    public View onCreateView(LayoutInflater inflater,
299            ViewGroup container, Bundle savedInstanceState) {
300
301        if (savedInstanceState != null) {
302            mViewState = savedInstanceState.getParcelable(BUNDLE_VIEW_STATE);
303        } else {
304            mViewState = new ConversationViewState();
305        }
306
307        View rootView = inflater.inflate(R.layout.conversation_view, container, false);
308        mConversationContainer = (ConversationContainer) rootView
309                .findViewById(R.id.conversation_container);
310
311        mNewMessageBar = mConversationContainer.findViewById(R.id.new_message_notification_bar);
312        mNewMessageBar.setOnClickListener(new View.OnClickListener() {
313            @Override
314            public void onClick(View v) {
315                onNewMessageBarClick();
316            }
317        });
318
319        mWebView = (ConversationWebView) mConversationContainer.findViewById(R.id.webview);
320
321        mWebView.addJavascriptInterface(mJsBridge, "mail");
322        mWebView.setWebViewClient(mWebViewClient);
323        mWebView.setWebChromeClient(new WebChromeClient() {
324            @Override
325            public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
326                LogUtils.i(LOG_TAG, "JS: %s (%s:%d)", consoleMessage.message(),
327                        consoleMessage.sourceId(), consoleMessage.lineNumber());
328                return true;
329            }
330        });
331        mWebView.setContentSizeChangeListener(new ConversationWebView.ContentSizeChangeListener() {
332            @Override
333            public void onHeightChange(int h) {
334                // When WebKit says the DOM height has changed, re-measure bodies and re-position
335                // their headers.
336                // This is separate from the typical JavaScript DOM change listeners because
337                // cases like NARROW_COLUMNS text reflow do not trigger DOM events.
338                mWebView.loadUrl("javascript:measurePositions();");
339            }
340        });
341
342        final WebSettings settings = mWebView.getSettings();
343
344        settings.setJavaScriptEnabled(true);
345        settings.setUseWideViewPort(true);
346        settings.setLoadWithOverviewMode(true);
347
348        settings.setSupportZoom(true);
349        settings.setBuiltInZoomControls(true);
350        settings.setDisplayZoomControls(false);
351
352        final float fontScale = getResources().getConfiguration().fontScale;
353        final int desiredFontSizePx = getResources()
354                .getInteger(R.integer.conversation_desired_font_size_px);
355        final int unstyledFontSizePx = getResources()
356                .getInteger(R.integer.conversation_unstyled_font_size_px);
357
358        int textZoom = settings.getTextZoom();
359        // apply a correction to the default body text style to get regular text to the size we want
360        textZoom = textZoom * desiredFontSizePx / unstyledFontSizePx;
361        // then apply any system font scaling
362        textZoom = (int) (textZoom * fontScale);
363        settings.setTextZoom(textZoom);
364
365        mViewsCreated = true;
366
367        return rootView;
368    }
369
370    @Override
371    public void onResume() {
372        super.onResume();
373
374        // Hacky workaround for http://b/6946182
375        Utils.fixSubTreeLayoutIfOrphaned(getView(), "ConversationViewFragment");
376    }
377
378    @Override
379    public void onSaveInstanceState(Bundle outState) {
380        if (mViewState != null) {
381            outState.putParcelable(BUNDLE_VIEW_STATE, mViewState);
382        }
383    }
384
385    @Override
386    public void onDestroyView() {
387        super.onDestroyView();
388        mConversationContainer.setOverlayAdapter(null);
389        mAdapter = null;
390        if (mMarkReadObserver != null) {
391            mActivity.getConversationUpdater().unregisterConversationListObserver(
392                    mMarkReadObserver);
393            mMarkReadObserver = null;
394        }
395        mViewsCreated = false;
396        mAccountObserver.unregisterAndDestroy();
397    }
398
399    @Override
400    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
401        super.onCreateOptionsMenu(menu, inflater);
402
403        mChangeFoldersMenuItem = menu.findItem(R.id.change_folder);
404    }
405
406    @Override
407    public void onPrepareOptionsMenu(Menu menu) {
408        super.onPrepareOptionsMenu(menu);
409        final boolean showMarkImportant = !mConversation.isImportant();
410        Utils.setMenuItemVisibility(menu, R.id.mark_important, showMarkImportant
411                && mAccount.supportsCapability(UIProvider.AccountCapabilities.MARK_IMPORTANT));
412        Utils.setMenuItemVisibility(menu, R.id.mark_not_important, !showMarkImportant
413                && mAccount.supportsCapability(UIProvider.AccountCapabilities.MARK_IMPORTANT));
414        Utils.setMenuItemVisibility(menu, R.id.delete,
415                mFolder != null && mFolder.supportsCapability(
416                        UIProvider.FolderCapabilities.DELETE));
417        final boolean archiveVisible = mAccount.supportsCapability(AccountCapabilities.ARCHIVE)
418                && mFolder != null && mFolder.supportsCapability(FolderCapabilities.ARCHIVE)
419                && !mFolder.isTrash();
420        Utils.setMenuItemVisibility(menu, R.id.archive, archiveVisible);
421        Utils.setMenuItemVisibility(menu, R.id.remove_folder, !archiveVisible && mFolder != null
422                && mFolder.supportsCapability(FolderCapabilities.CAN_ACCEPT_MOVED_MESSAGES)
423                && !mFolder.isProviderFolder());
424        final MenuItem removeFolder = menu.findItem(R.id.remove_folder);
425        if (removeFolder != null) {
426            removeFolder.setTitle(getString(R.string.remove_folder, mFolder.name));
427        }
428        Utils.setMenuItemVisibility(menu, R.id.report_spam,
429                mAccount.supportsCapability(AccountCapabilities.REPORT_SPAM) && mFolder != null
430                        && mFolder.supportsCapability(FolderCapabilities.REPORT_SPAM)
431                        && !mConversation.spam);
432        Utils.setMenuItemVisibility(menu, R.id.mark_not_spam,
433                mAccount.supportsCapability(AccountCapabilities.REPORT_SPAM) && mFolder != null
434                        && mFolder.supportsCapability(FolderCapabilities.MARK_NOT_SPAM)
435                        && mConversation.spam);
436        Utils.setMenuItemVisibility(menu, R.id.report_phishing,
437                mAccount.supportsCapability(AccountCapabilities.REPORT_PHISHING) && mFolder != null
438                        && mFolder.supportsCapability(FolderCapabilities.REPORT_PHISHING)
439                        && !mConversation.phishing);
440        Utils.setMenuItemVisibility(menu, R.id.mute,
441                        mAccount.supportsCapability(AccountCapabilities.MUTE) && mFolder != null
442                        && mFolder.supportsCapability(FolderCapabilities.DESTRUCTIVE_MUTE)
443                        && !mConversation.muted);
444    }
445
446    @Override
447    public boolean onOptionsItemSelected(MenuItem item) {
448        boolean handled = false;
449
450        switch (item.getItemId()) {
451            case R.id.inside_conversation_unread:
452                markUnread();
453                handled = true;
454                break;
455        }
456
457        return handled;
458    }
459
460    @Override
461    public ConversationUpdater getListController() {
462        final ControllableActivity activity = (ControllableActivity) getActivity();
463        return activity != null ? activity.getConversationUpdater() : null;
464    }
465
466    @Override
467    public MessageCursor getMessageCursor() {
468        return mCursor;
469    }
470
471    private void markUnread() {
472        // Ignore unsafe calls made after a fragment is detached from an activity
473        final ControllableActivity activity = (ControllableActivity) getActivity();
474        if (activity == null) {
475            LogUtils.w(LOG_TAG, "ignoring markUnread for conv=%s", mConversation.id);
476            return;
477        }
478
479        if (mViewState == null) {
480            LogUtils.i(LOG_TAG, "ignoring markUnread for conv with no view state (%d)",
481                    mConversation.id);
482            return;
483        }
484        activity.getConversationUpdater().markConversationMessagesUnread(mConversation,
485                mViewState.getUnreadMessageUris(), mViewState.getConversationInfo());
486    }
487
488    /**
489     * {@link #setUserVisibleHint(boolean)} only works on API >= 15, so implement our own for
490     * reliability on older platforms.
491     */
492    public void setExtraUserVisibleHint(boolean isVisibleToUser) {
493        LogUtils.v(LOG_TAG, "in CVF.setHint, val=%s (%s)", isVisibleToUser, this);
494
495        if (mUserVisible != isVisibleToUser) {
496            mUserVisible = isVisibleToUser;
497
498            if (isVisibleToUser && mViewsCreated) {
499
500                if (mCursor == null && mDeferredConversationLoad) {
501                    // load
502                    LogUtils.v(LOG_TAG, "Fragment is now user-visible, showing conversation: %s",
503                            mConversation.uri);
504                    showConversation();
505                    mDeferredConversationLoad = false;
506                } else {
507                    onConversationSeen();
508                }
509
510            }
511        }
512    }
513
514    /**
515     * Handles a request to show a new conversation list, either from a search query or for viewing
516     * a folder. This will initiate a data load, and hence must be called on the UI thread.
517     */
518    private void showConversation() {
519        final boolean disableOffscreenLoading = DISABLE_OFFSCREEN_LOADING ||
520                (mConversation.isRemote || mConversation.getNumMessages() > mMaxAutoLoadMessages);
521        if (!mUserVisible && disableOffscreenLoading) {
522            LogUtils.v(LOG_TAG, "Fragment not user-visible, not showing conversation: %s",
523                    mConversation.uri);
524            mDeferredConversationLoad = true;
525            return;
526        }
527        LogUtils.v(LOG_TAG,
528                "Fragment is short or user-visible, immediately rendering conversation: %s",
529                mConversation.uri);
530        getLoaderManager().initLoader(MESSAGE_LOADER_ID, Bundle.EMPTY, mMessageLoaderCallbacks);
531
532        if (mUserVisible) {
533            final SubjectDisplayChanger sdc = mActivity.getSubjectDisplayChanger();
534            if (sdc != null) {
535                sdc.setSubject(mConversation.subject);
536            }
537        }
538    }
539
540    public Conversation getConversation() {
541        return mConversation;
542    }
543
544    private void renderConversation(MessageCursor messageCursor) {
545        final String convHtml = renderMessageBodies(messageCursor);
546
547        if (DEBUG_DUMP_CONVERSATION_HTML) {
548            java.io.FileWriter fw = null;
549            try {
550                fw = new java.io.FileWriter("/sdcard/conv" + mConversation.id
551                        + ".html");
552                fw.write(convHtml);
553            } catch (java.io.IOException e) {
554                e.printStackTrace();
555            } finally {
556                if (fw != null) {
557                    try {
558                        fw.close();
559                    } catch (java.io.IOException e) {
560                        e.printStackTrace();
561                    }
562                }
563            }
564        }
565
566        mWebView.loadDataWithBaseURL(mBaseUri, convHtml, "text/html", "utf-8", null);
567        mCursor = messageCursor;
568    }
569
570    /**
571     * Populate the adapter with overlay views (message headers, super-collapsed blocks, a
572     * conversation header), and return an HTML document with spacer divs inserted for all overlays.
573     *
574     */
575    private String renderMessageBodies(MessageCursor messageCursor) {
576        int pos = -1;
577
578        LogUtils.d(LOG_TAG, "IN renderMessageBodies, fragment=%s subj=%s", this,
579                mConversation.subject);
580        boolean allowNetworkImages = false;
581
582        // TODO: re-use any existing adapter item state (expanded, details expanded, show pics)
583
584        // Walk through the cursor and build up an overlay adapter as you go.
585        // Each overlay has an entry in the adapter for easy scroll handling in the container.
586        // Items are not necessarily 1:1 in cursor and adapter because of super-collapsed blocks.
587        // When adding adapter items, also add their heights to help the container later determine
588        // overlay dimensions.
589
590        mAdapter.clear();
591
592        // re-evaluate the message parts of the view state, since the messages may have changed
593        // since the previous render
594        final ConversationViewState prevState = mViewState;
595        mViewState = new ConversationViewState(prevState);
596
597        // N.B. the units of height for spacers are actually dp and not px because WebView assumes
598        // a pixel is an mdpi pixel, unless you set device-dpi.
599
600        // add a single conversation header item
601        final int convHeaderPos = mAdapter.addConversationHeader(mConversation);
602        final int convHeaderPx = measureOverlayHeight(convHeaderPos);
603
604        mTemplates.startConversation(mWebView.screenPxToWebPx(convHeaderPx));
605
606        int collapsedStart = -1;
607        ConversationMessage prevCollapsedMsg = null;
608        boolean prevSafeForImages = false;
609
610        while (messageCursor.moveToPosition(++pos)) {
611            final ConversationMessage msg = messageCursor.getMessage();
612
613            // TODO: save/restore 'show pics' state
614            final boolean safeForImages = msg.alwaysShowImages /* || savedStateSaysSafe */;
615            allowNetworkImages |= safeForImages;
616
617            final Integer savedExpanded = prevState.getExpansionState(msg);
618            final int expandedState;
619            if (savedExpanded != null) {
620                expandedState = savedExpanded;
621            } else {
622                // new messages that are not expanded default to being eligible for super-collapse
623                expandedState = (!msg.read || msg.starred || messageCursor.isLast()) ?
624                        ExpansionState.EXPANDED : ExpansionState.SUPER_COLLAPSED;
625            }
626            mViewState.setExpansionState(msg, expandedState);
627
628            // save off "read" state from the cursor
629            // later, the view may not match the cursor (e.g. conversation marked read on open)
630            // however, if a previous state indicated this message was unread, trust that instead
631            // so "mark unread" marks all originally unread messages
632            mViewState.setReadState(msg, msg.read && !prevState.isUnread(msg));
633
634            // We only want to consider this for inclusion in the super collapsed block if
635            // 1) The we don't have previous state about this message  (The first time that the
636            //    user opens a conversation)
637            // 2) The previously saved state for this message indicates that this message is
638            //    in the super collapsed block.
639            if (ExpansionState.isSuperCollapsed(expandedState)) {
640                // contribute to a super-collapsed block that will be emitted just before the
641                // next expanded header
642                if (collapsedStart < 0) {
643                    collapsedStart = pos;
644                }
645                prevCollapsedMsg = msg;
646                prevSafeForImages = safeForImages;
647                continue;
648            }
649
650            // resolve any deferred decisions on previous collapsed items
651            if (collapsedStart >= 0) {
652                if (pos - collapsedStart == 1) {
653                    // special-case for a single collapsed message: no need to super-collapse it
654                    renderMessage(prevCollapsedMsg, false /* expanded */,
655                            prevSafeForImages);
656                } else {
657                    renderSuperCollapsedBlock(collapsedStart, pos - 1);
658                }
659                prevCollapsedMsg = null;
660                collapsedStart = -1;
661            }
662
663            renderMessage(msg, ExpansionState.isExpanded(expandedState), safeForImages);
664        }
665
666        mWebView.getSettings().setBlockNetworkImage(!allowNetworkImages);
667
668        // If the conversation has specified a base uri, use it here, use mBaseUri
669        final String conversationBaseUri = mConversation.conversationBaseUri != null ?
670                mConversation.conversationBaseUri.toString() : mBaseUri;
671        return mTemplates.endConversation(mBaseUri, conversationBaseUri, 320,
672                mWebView.getViewportWidth());
673    }
674
675    private void renderSuperCollapsedBlock(int start, int end) {
676        final int blockPos = mAdapter.addSuperCollapsedBlock(start, end);
677        final int blockPx = measureOverlayHeight(blockPos);
678        mTemplates.appendSuperCollapsedHtml(start, mWebView.screenPxToWebPx(blockPx));
679    }
680
681    private void renderMessage(ConversationMessage msg, boolean expanded,
682            boolean safeForImages) {
683        final int headerPos = mAdapter.addMessageHeader(msg, expanded);
684        final MessageHeaderItem headerItem = (MessageHeaderItem) mAdapter.getItem(headerPos);
685
686        final int footerPos = mAdapter.addMessageFooter(headerItem);
687
688        // Measure item header and footer heights to allocate spacers in HTML
689        // But since the views themselves don't exist yet, render each item temporarily into
690        // a host view for measurement.
691        final int headerPx = measureOverlayHeight(headerPos);
692        final int footerPx = measureOverlayHeight(footerPos);
693
694        mTemplates.appendMessageHtml(msg, expanded, safeForImages, 1.0f,
695                mWebView.screenPxToWebPx(headerPx), mWebView.screenPxToWebPx(footerPx));
696    }
697
698    private String renderCollapsedHeaders(MessageCursor cursor,
699            SuperCollapsedBlockItem blockToReplace) {
700        final List<ConversationOverlayItem> replacements = Lists.newArrayList();
701
702        mTemplates.reset();
703
704        for (int i = blockToReplace.getStart(), end = blockToReplace.getEnd(); i <= end; i++) {
705            cursor.moveToPosition(i);
706            final ConversationMessage msg = cursor.getMessage();
707            final MessageHeaderItem header = mAdapter.newMessageHeaderItem(msg,
708                    false /* expanded */);
709            final MessageFooterItem footer = mAdapter.newMessageFooterItem(header);
710
711            final int headerPx = measureOverlayHeight(header);
712            final int footerPx = measureOverlayHeight(footer);
713
714            mTemplates.appendMessageHtml(msg, false /* expanded */, msg.alwaysShowImages, 1.0f,
715                    mWebView.screenPxToWebPx(headerPx), mWebView.screenPxToWebPx(footerPx));
716            replacements.add(header);
717            replacements.add(footer);
718
719            mViewState.setExpansionState(msg, ExpansionState.COLLAPSED);
720        }
721
722        mAdapter.replaceSuperCollapsedBlock(blockToReplace, replacements);
723
724        return mTemplates.emit();
725    }
726
727    private int measureOverlayHeight(int position) {
728        return measureOverlayHeight(mAdapter.getItem(position));
729    }
730
731    /**
732     * Measure the height of an adapter view by rendering an adapter item into a temporary
733     * host view, and asking the view to immediately measure itself. This method will reuse
734     * a previous adapter view from {@link ConversationContainer}'s scrap views if one was generated
735     * earlier.
736     * <p>
737     * After measuring the height, this method also saves the height in the
738     * {@link ConversationOverlayItem} for later use in overlay positioning.
739     *
740     * @param convItem adapter item with data to render and measure
741     * @return height of the rendered view in screen px
742     */
743    private int measureOverlayHeight(ConversationOverlayItem convItem) {
744        final int type = convItem.getType();
745
746        final View convertView = mConversationContainer.getScrapView(type);
747        final View hostView = mAdapter.getView(convItem, convertView, mConversationContainer,
748                true /* measureOnly */);
749        if (convertView == null) {
750            mConversationContainer.addScrapView(type, hostView);
751        }
752
753        final int heightPx = mConversationContainer.measureOverlay(hostView);
754        convItem.setHeight(heightPx);
755        convItem.markMeasurementValid();
756
757        return heightPx;
758    }
759
760    private void onConversationSeen() {
761        // Ignore unsafe calls made after a fragment is detached from an activity
762        final ControllableActivity activity = (ControllableActivity) getActivity();
763        if (activity == null) {
764            LogUtils.w(LOG_TAG, "ignoring onConversationSeen for conv=%s", mConversation.id);
765            return;
766        }
767
768        mViewState.setInfoForConversation(mConversation);
769
770        // mark viewed/read if not previously marked viewed by this conversation view,
771        // or if unread messages still exist in the message list cursor
772        // we don't want to keep marking viewed on rotation or restore
773        // but we do want future re-renders to mark read (e.g. "New message from X" case)
774        if (!mConversation.isViewed() || (mCursor != null && !mCursor.isConversationRead())) {
775            final ConversationUpdater listController = activity.getConversationUpdater();
776            // The conversation cursor may not have finished loading by now (when launched via
777            // notification), so watch for when it finishes and mark it read then.
778            if (listController.getConversationListCursor() == null) {
779                LogUtils.i(LOG_TAG, "deferring conv mark read on open for id=%d",
780                        mConversation.id);
781                mMarkReadObserver = new MarkReadObserver(listController);
782                listController.registerConversationListObserver(mMarkReadObserver);
783            } else {
784                markReadOnSeen(listController);
785            }
786        }
787
788        activity.onConversationSeen(mConversation);
789
790        final SubjectDisplayChanger sdc = activity.getSubjectDisplayChanger();
791        if (sdc != null) {
792            sdc.setSubject(mConversation.subject);
793        }
794    }
795
796    private void markReadOnSeen(ConversationUpdater listController) {
797        // Mark the conversation viewed and read.
798        listController.markConversationsRead(Arrays.asList(mConversation), true /* read */,
799                true /* viewed */);
800
801        // and update the Message objects in the cursor so the next time a cursor update happens
802        // with these messages marked read, we know to ignore it
803        if (mCursor != null) {
804            mCursor.markMessagesRead();
805        }
806    }
807
808    // BEGIN conversation header callbacks
809    @Override
810    public void onFoldersClicked() {
811        if (mChangeFoldersMenuItem == null) {
812            LogUtils.e(LOG_TAG, "unable to open 'change folders' dialog for a conversation");
813            return;
814        }
815        mActivity.onOptionsItemSelected(mChangeFoldersMenuItem);
816    }
817
818    @Override
819    public void onConversationViewHeaderHeightChange(int newHeight) {
820        // TODO: propagate the new height to the header's HTML spacer. This can happen when labels
821        // are added/removed
822    }
823
824    @Override
825    public String getSubjectRemainder(String subject) {
826        final SubjectDisplayChanger sdc = mActivity.getSubjectDisplayChanger();
827        if (sdc == null) {
828            return subject;
829        }
830        return sdc.getUnshownSubject(subject);
831    }
832    // END conversation header callbacks
833
834    // START message header callbacks
835    @Override
836    public void setMessageSpacerHeight(MessageHeaderItem item, int newSpacerHeightPx) {
837        mConversationContainer.invalidateSpacerGeometry();
838
839        // update message HTML spacer height
840        final int h = mWebView.screenPxToWebPx(newSpacerHeightPx);
841        LogUtils.i(LAYOUT_TAG, "setting HTML spacer h=%dwebPx (%dscreenPx)", h,
842                newSpacerHeightPx);
843        mWebView.loadUrl(String.format("javascript:setMessageHeaderSpacerHeight('%s', %d);",
844                mTemplates.getMessageDomId(item.message), h));
845    }
846
847    @Override
848    public void setMessageExpanded(MessageHeaderItem item, int newSpacerHeightPx) {
849        mConversationContainer.invalidateSpacerGeometry();
850
851        // show/hide the HTML message body and update the spacer height
852        final int h = mWebView.screenPxToWebPx(newSpacerHeightPx);
853        LogUtils.i(LAYOUT_TAG, "setting HTML spacer expanded=%s h=%dwebPx (%dscreenPx)",
854                item.isExpanded(), h, newSpacerHeightPx);
855        mWebView.loadUrl(String.format("javascript:setMessageBodyVisible('%s', %s, %d);",
856                mTemplates.getMessageDomId(item.message), item.isExpanded(), h));
857
858        mViewState.setExpansionState(item.message,
859                item.isExpanded() ? ExpansionState.EXPANDED : ExpansionState.COLLAPSED);
860    }
861
862    @Override
863    public void showExternalResources(Message msg) {
864        mWebView.getSettings().setBlockNetworkImage(false);
865        mWebView.loadUrl("javascript:unblockImages('" + mTemplates.getMessageDomId(msg) + "');");
866    }
867    // END message header callbacks
868
869    @Override
870    public void onSuperCollapsedClick(SuperCollapsedBlockItem item) {
871        if (mCursor == null || !mViewsCreated) {
872            return;
873        }
874
875        mTempBodiesHtml = renderCollapsedHeaders(mCursor, item);
876        mWebView.loadUrl("javascript:replaceSuperCollapsedBlock(" + item.getStart() + ")");
877    }
878
879    private void showNewMessageNotification(NewMessagesInfo info) {
880        final TextView descriptionView = (TextView) mNewMessageBar.findViewById(
881                R.id.new_message_description);
882        descriptionView.setText(info.getNotificationText());
883        mNewMessageBar.setVisibility(View.VISIBLE);
884    }
885
886    private void onNewMessageBarClick() {
887        mNewMessageBar.setVisibility(View.GONE);
888
889        renderConversation(mCursor); // mCursor is already up-to-date per onLoadFinished()
890    }
891
892    private static class MessageLoader extends CursorLoader {
893        private boolean mDeliveredFirstResults = false;
894        private final Conversation mConversation;
895        private final ConversationController mController;
896
897        public MessageLoader(Context c, Conversation conv, ConversationController controller) {
898            super(c, conv.messageListUri, UIProvider.MESSAGE_PROJECTION, null, null, null);
899            mConversation = conv;
900            mController = controller;
901        }
902
903        @Override
904        public Cursor loadInBackground() {
905            return new MessageCursor(super.loadInBackground(), mConversation, mController);
906        }
907
908        @Override
909        public void deliverResult(Cursor result) {
910            // We want to deliver these results, and then we want to make sure that any subsequent
911            // queries do not hit the network
912            super.deliverResult(result);
913
914            if (!mDeliveredFirstResults) {
915                mDeliveredFirstResults = true;
916                Uri uri = getUri();
917
918                // Create a ListParams that tells the provider to not hit the network
919                final ListParams listParams =
920                        new ListParams(ListParams.NO_LIMIT, false /* useNetwork */);
921
922                // Build the new uri with this additional parameter
923                uri = uri.buildUpon().appendQueryParameter(
924                        UIProvider.LIST_PARAMS_QUERY_PARAMETER, listParams.serialize()).build();
925                setUri(uri);
926            }
927        }
928    }
929
930    private static int[] parseInts(final String[] stringArray) {
931        final int len = stringArray.length;
932        final int[] ints = new int[len];
933        for (int i = 0; i < len; i++) {
934            ints[i] = Integer.parseInt(stringArray[i]);
935        }
936        return ints;
937    }
938
939    @Override
940    public String toString() {
941        // log extra info at DEBUG level or finer
942        final String s = super.toString();
943        if (!LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG) || mConversation == null) {
944            return s;
945        }
946        return "(" + s + " subj=" + mConversation.subject + ")";
947    }
948
949    private Address getAddress(String rawFrom) {
950        Address addr = mAddressCache.get(rawFrom);
951        if (addr == null) {
952            addr = Address.getEmailAddress(rawFrom);
953            mAddressCache.put(rawFrom, addr);
954        }
955        return addr;
956    }
957
958    @Override
959    public Account getAccount() {
960        return mAccount;
961    }
962
963    private class ConversationWebViewClient extends WebViewClient {
964
965        @Override
966        public void onPageFinished(WebView view, String url) {
967            // Ignore unsafe calls made after a fragment is detached from an activity
968            final ControllableActivity activity = (ControllableActivity) getActivity();
969            if (activity == null || !mViewsCreated) {
970                LogUtils.i(LOG_TAG, "ignoring CVF.onPageFinished, url=%s fragment=%s", url,
971                        ConversationViewFragment.this);
972                return;
973            }
974
975            LogUtils.i(LOG_TAG, "IN CVF.onPageFinished, url=%s fragment=%s act=%s", url,
976                    ConversationViewFragment.this, getActivity());
977
978            super.onPageFinished(view, url);
979
980            // TODO: save off individual message unread state (here, or in onLoadFinished?) so
981            // 'mark unread' restores the original unread state for each individual message
982
983            if (mUserVisible) {
984                onConversationSeen();
985            }
986
987            final Set<String> emailAddresses = Sets.newHashSet();
988            for (Address addr : mAddressCache.values()) {
989                emailAddresses.add(addr.getAddress());
990            }
991            mContactLoaderCallbacks.setSenders(emailAddresses);
992            getLoaderManager().restartLoader(CONTACT_LOADER_ID, Bundle.EMPTY,
993                    mContactLoaderCallbacks);
994        }
995
996        @Override
997        public boolean shouldOverrideUrlLoading(WebView view, String url) {
998            final Activity activity = getActivity();
999            if (!mViewsCreated || activity == null) {
1000                return false;
1001            }
1002
1003            boolean result = false;
1004            final Uri uri = Uri.parse(url);
1005            Intent intent = new Intent(Intent.ACTION_VIEW, uri);
1006            intent.putExtra(Browser.EXTRA_APPLICATION_ID, activity.getPackageName());
1007            intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
1008
1009            // FIXME: give provider a chance to customize url intents?
1010            // Utils.addGoogleUriAccountIntentExtras(mContext, uri, mAccount, intent);
1011
1012            try {
1013                activity.startActivity(intent);
1014                result = true;
1015            } catch (ActivityNotFoundException ex) {
1016                // If no application can handle the URL, assume that the
1017                // caller can handle it.
1018            }
1019
1020            return result;
1021        }
1022
1023    }
1024
1025    /**
1026     * NOTE: all public methods must be listed in the proguard flags so that they can be accessed
1027     * via reflection and not stripped.
1028     *
1029     */
1030    private class MailJsBridge {
1031
1032        @SuppressWarnings("unused")
1033        public void onWebContentGeometryChange(final String[] overlayBottomStrs) {
1034            try {
1035                mHandler.post(new Runnable() {
1036                    @Override
1037                    public void run() {
1038                        if (!mViewsCreated) {
1039                            LogUtils.d(LOG_TAG, "ignoring webContentGeometryChange because views" +
1040                                    " are gone, %s", ConversationViewFragment.this);
1041                            return;
1042                        }
1043
1044                        mConversationContainer.onGeometryChange(parseInts(overlayBottomStrs));
1045                    }
1046                });
1047            } catch (Throwable t) {
1048                LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.onWebContentGeometryChange");
1049            }
1050        }
1051
1052        @SuppressWarnings("unused")
1053        public String getTempMessageBodies() {
1054            try {
1055                if (!mViewsCreated) {
1056                    return "";
1057                }
1058
1059                final String s = mTempBodiesHtml;
1060                mTempBodiesHtml = null;
1061                return s;
1062            } catch (Throwable t) {
1063                LogUtils.e(LOG_TAG, t, "Error in MailJsBridge.getTempMessageBodies");
1064                return "";
1065            }
1066        }
1067
1068    }
1069
1070    private class NewMessagesInfo {
1071        int count;
1072        String senderAddress;
1073
1074        /**
1075         * Return the display text for the new message notification overlay. It will be formatted
1076         * appropriately for a single new message vs. multiple new messages.
1077         *
1078         * @return display text
1079         */
1080        public String getNotificationText() {
1081            final Object param;
1082            if (count > 1) {
1083                param = count;
1084            } else {
1085                final Address addr = getAddress(senderAddress);
1086                param = TextUtils.isEmpty(addr.getName()) ? addr.getAddress() : addr.getName();
1087            }
1088            return getResources().getQuantityString(R.plurals.new_incoming_messages, count, param);
1089        }
1090    }
1091
1092    private class MessageLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
1093
1094        @Override
1095        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
1096            return new MessageLoader(mContext, mConversation, ConversationViewFragment.this);
1097        }
1098
1099        @Override
1100        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
1101            MessageCursor messageCursor = (MessageCursor) data;
1102
1103            // ignore truly duplicate results
1104            // this can happen when restoring after rotation
1105            if (mCursor == messageCursor) {
1106                return;
1107            }
1108
1109            if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
1110                LogUtils.d(LOG_TAG, "LOADED CONVERSATION= %s", messageCursor.getDebugDump());
1111            }
1112
1113            // ignore cursors that are still loading results
1114            if (!messageCursor.isLoaded()) {
1115                return;
1116            }
1117
1118            // TODO: handle ERROR status
1119
1120            if (messageCursor.getCount() == 0 && mCursor != null) {
1121                // TODO: need to exit this view- conversation may have been deleted, or for
1122                // whatever reason is now invalid (e.g. discard single draft)
1123                return;
1124            }
1125
1126            /*
1127             * what kind of changes affect the MessageCursor?
1128             * 1. new message(s)
1129             * 2. read/unread state change
1130             * 3. deleted message, either regular or draft
1131             * 4. updated message, either from self or from others, updated in content or state
1132             * or sender
1133             * 5. star/unstar of message (technically similar to #1)
1134             * 6. other label change
1135             *
1136             * Use MessageCursor.hashCode() to sort out interesting vs. no-op cursor updates.
1137             */
1138
1139            if (mCursor == null) {
1140                LogUtils.i(LOG_TAG, "CONV RENDER: existing cursor is null, rendering from scratch");
1141            } else {
1142                final NewMessagesInfo info = getNewIncomingMessagesInfo(messageCursor);
1143
1144                if (info.count > 0 || messageCursor.hashCode() == mCursor.hashCode()) {
1145
1146                    if (info.count > 0) {
1147                        // don't immediately render new incoming messages from other senders
1148                        // (to avoid a new message from losing the user's focus)
1149                        LogUtils.i(LOG_TAG, "CONV RENDER: conversation updated"
1150                                + ", holding cursor for new incoming message");
1151                        showNewMessageNotification(info);
1152                    } else {
1153                        LogUtils.i(LOG_TAG, "CONV RENDER: uninteresting update"
1154                                + ", ignoring this conversation update");
1155                    }
1156
1157                    // update mCursor reference because the old one is about to be closed by
1158                    // CursorLoader
1159                    mCursor = messageCursor;
1160                    return;
1161                }
1162
1163                // cursors are different, and not due to an incoming message. fall through and
1164                // render.
1165                LogUtils.i(LOG_TAG, "CONV RENDER: conversation updated"
1166                        + ", but not due to incoming message. rendering.");
1167            }
1168
1169            renderConversation(messageCursor);
1170
1171            // TODO: if this is not user-visible, delay render until user-visible fragment is done.
1172            // This is needed in addition to the showConversation() delay to speed up rotation and
1173            // restoration.
1174        }
1175
1176        @Override
1177        public void onLoaderReset(Loader<Cursor> loader) {
1178            mCursor = null;
1179        }
1180
1181        private NewMessagesInfo getNewIncomingMessagesInfo(MessageCursor newCursor) {
1182            final NewMessagesInfo info = new NewMessagesInfo();
1183
1184            int pos = -1;
1185            while (newCursor.moveToPosition(++pos)) {
1186                final Message m = newCursor.getMessage();
1187                if (!mViewState.contains(m)) {
1188                    LogUtils.i(LOG_TAG, "conversation diff: found new msg: %s", m.uri);
1189
1190                    final Address from = getAddress(m.from);
1191                    // distinguish ours from theirs
1192                    // new messages from the account owner should not trigger a notification
1193                    if (mAccount.ownsFromAddress(from.getAddress())) {
1194                        LogUtils.i(LOG_TAG, "found message from self: %s", m.uri);
1195                        continue;
1196                    }
1197
1198                    info.count++;
1199                    info.senderAddress = m.from;
1200                }
1201            }
1202            return info;
1203        }
1204
1205    }
1206
1207    /**
1208     * Inner class to to asynchronously load contact data for all senders in the conversation,
1209     * and notify observers when the data is ready.
1210     *
1211     */
1212    private class ContactLoaderCallbacks implements ContactInfoSource,
1213            LoaderManager.LoaderCallbacks<ImmutableMap<String, ContactInfo>> {
1214
1215        private Set<String> mSenders;
1216        private ImmutableMap<String, ContactInfo> mContactInfoMap;
1217        private DataSetObservable mObservable = new DataSetObservable();
1218
1219        public void setSenders(Set<String> emailAddresses) {
1220            mSenders = emailAddresses;
1221        }
1222
1223        @Override
1224        public Loader<ImmutableMap<String, ContactInfo>> onCreateLoader(int id, Bundle args) {
1225            return new SenderInfoLoader(mContext, mSenders);
1226        }
1227
1228        @Override
1229        public void onLoadFinished(Loader<ImmutableMap<String, ContactInfo>> loader,
1230                ImmutableMap<String, ContactInfo> data) {
1231            mContactInfoMap = data;
1232            mObservable.notifyChanged();
1233        }
1234
1235        @Override
1236        public void onLoaderReset(Loader<ImmutableMap<String, ContactInfo>> loader) {
1237        }
1238
1239        @Override
1240        public ContactInfo getContactInfo(String email) {
1241            if (mContactInfoMap == null) {
1242                return null;
1243            }
1244            return mContactInfoMap.get(email);
1245        }
1246
1247        @Override
1248        public void registerObserver(DataSetObserver observer) {
1249            mObservable.registerObserver(observer);
1250        }
1251
1252        @Override
1253        public void unregisterObserver(DataSetObserver observer) {
1254            mObservable.unregisterObserver(observer);
1255        }
1256
1257    }
1258
1259    private class MarkReadObserver extends DataSetObserver {
1260        private final ConversationUpdater mListController;
1261
1262        private MarkReadObserver(ConversationUpdater listController) {
1263            mListController = listController;
1264        }
1265
1266        @Override
1267        public void onChanged() {
1268            if (mListController.getConversationListCursor() == null) {
1269                // nothing yet, keep watching
1270                return;
1271            }
1272            // done loading, safe to mark read now
1273            mListController.unregisterConversationListObserver(this);
1274            mMarkReadObserver = null;
1275            LogUtils.i(LOG_TAG, "running deferred conv mark read on open, id=%d", mConversation.id);
1276            markReadOnSeen(mListController);
1277        }
1278    }
1279
1280    private class SetCookieTask extends AsyncTask<Void, Void, Void> {
1281        final String mUri;
1282        final String mCookie;
1283
1284        SetCookieTask(String uri, String cookie) {
1285            mUri = uri;
1286            mCookie = cookie;
1287        }
1288
1289        @Override
1290        public Void doInBackground(Void... args) {
1291            final CookieSyncManager csm =
1292                CookieSyncManager.createInstance(mContext);
1293            CookieManager.getInstance().setCookie(mUri, mCookie);
1294            csm.sync();
1295            return null;
1296        }
1297    }
1298}
1299