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