ComposeActivity.java revision f6c00b8304062a17d14b484bd1fc33a83c4a90d7
1/** 2 * Copyright (c) 2011, Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.mail.compose; 18 19import android.app.ActionBar; 20import android.app.ActionBar.OnNavigationListener; 21import android.app.Activity; 22import android.app.ActivityManager; 23import android.app.AlertDialog; 24import android.app.Dialog; 25import android.app.Fragment; 26import android.app.FragmentTransaction; 27import android.app.LoaderManager; 28import android.content.ContentResolver; 29import android.content.ContentValues; 30import android.content.Context; 31import android.content.CursorLoader; 32import android.content.DialogInterface; 33import android.content.Intent; 34import android.content.Loader; 35import android.content.pm.ActivityInfo; 36import android.database.Cursor; 37import android.net.Uri; 38import android.os.Bundle; 39import android.os.Handler; 40import android.os.HandlerThread; 41import android.os.Parcelable; 42import android.os.ParcelFileDescriptor; 43import android.provider.BaseColumns; 44import android.text.Editable; 45import android.text.Html; 46import android.text.SpannableString; 47import android.text.Spanned; 48import android.text.TextUtils; 49import android.text.TextWatcher; 50import android.text.util.Rfc822Token; 51import android.text.util.Rfc822Tokenizer; 52import android.view.Gravity; 53import android.view.KeyEvent; 54import android.view.LayoutInflater; 55import android.view.Menu; 56import android.view.MenuInflater; 57import android.view.MenuItem; 58import android.view.View; 59import android.view.View.OnClickListener; 60import android.view.ViewGroup; 61import android.view.inputmethod.EditorInfo; 62import android.widget.ArrayAdapter; 63import android.widget.Button; 64import android.widget.EditText; 65import android.widget.TextView; 66import android.widget.Toast; 67 68import com.android.common.Rfc822Validator; 69import com.android.ex.chips.RecipientEditTextView; 70import com.android.mail.R; 71import com.android.mail.compose.AttachmentsView.AttachmentAddedOrDeletedListener; 72import com.android.mail.compose.AttachmentsView.AttachmentFailureException; 73import com.android.mail.compose.FromAddressSpinner.OnAccountChangedListener; 74import com.android.mail.compose.QuotedTextView.RespondInlineListener; 75import com.android.mail.providers.Account; 76import com.android.mail.providers.Address; 77import com.android.mail.providers.Attachment; 78import com.android.mail.providers.MailAppProvider; 79import com.android.mail.providers.Message; 80import com.android.mail.providers.MessageModification; 81import com.android.mail.providers.ReplyFromAccount; 82import com.android.mail.providers.Settings; 83import com.android.mail.providers.UIProvider; 84import com.android.mail.providers.UIProvider.AccountCapabilities; 85import com.android.mail.providers.UIProvider.DraftType; 86import com.android.mail.ui.MailActivity; 87import com.android.mail.ui.WaitFragment; 88import com.android.mail.ui.AttachmentTile.AttachmentPreview; 89import com.android.mail.utils.AccountUtils; 90import com.android.mail.utils.AttachmentUtils; 91import com.android.mail.utils.ContentProviderTask; 92import com.android.mail.utils.LogTag; 93import com.android.mail.utils.LogUtils; 94import com.android.mail.utils.Utils; 95import com.google.common.annotations.VisibleForTesting; 96import com.google.common.collect.Lists; 97import com.google.common.collect.Sets; 98 99import java.io.FileNotFoundException; 100import java.io.IOException; 101import java.io.UnsupportedEncodingException; 102import java.net.URLDecoder; 103import java.util.ArrayList; 104import java.util.Arrays; 105import java.util.Collection; 106import java.util.HashMap; 107import java.util.HashSet; 108import java.util.List; 109import java.util.Map.Entry; 110import java.util.Set; 111import java.util.concurrent.ConcurrentHashMap; 112 113public class ComposeActivity extends Activity implements OnClickListener, OnNavigationListener, 114 RespondInlineListener, DialogInterface.OnClickListener, TextWatcher, 115 AttachmentAddedOrDeletedListener, OnAccountChangedListener, LoaderManager.LoaderCallbacks<Cursor>, 116 TextView.OnEditorActionListener { 117 // Identifiers for which type of composition this is 118 static final int COMPOSE = -1; 119 static final int REPLY = 0; 120 static final int REPLY_ALL = 1; 121 static final int FORWARD = 2; 122 static final int EDIT_DRAFT = 3; 123 124 // Integer extra holding one of the above compose action 125 protected static final String EXTRA_ACTION = "action"; 126 127 private static final String EXTRA_SHOW_CC = "showCc"; 128 private static final String EXTRA_SHOW_BCC = "showBcc"; 129 private static final String EXTRA_RESPONDED_INLINE = "respondedInline"; 130 private static final String EXTRA_SAVE_ENABLED = "saveEnabled"; 131 132 private static final String UTF8_ENCODING_NAME = "UTF-8"; 133 134 private static final String MAIL_TO = "mailto"; 135 136 private static final String EXTRA_SUBJECT = "subject"; 137 138 private static final String EXTRA_BODY = "body"; 139 140 protected static final String EXTRA_FROM_ACCOUNT_STRING = "fromAccountString"; 141 142 private static final String EXTRA_ATTACHMENT_PREVIEWS = "attachmentPreviews"; 143 144 // Extra that we can get passed from other activities 145 private static final String EXTRA_TO = "to"; 146 private static final String EXTRA_CC = "cc"; 147 private static final String EXTRA_BCC = "bcc"; 148 149 // List of all the fields 150 static final String[] ALL_EXTRAS = { EXTRA_SUBJECT, EXTRA_BODY, EXTRA_TO, EXTRA_CC, EXTRA_BCC }; 151 152 private static SendOrSaveCallback sTestSendOrSaveCallback = null; 153 // Map containing information about requests to create new messages, and the id of the 154 // messages that were the result of those requests. 155 // 156 // This map is used when the activity that initiated the save a of a new message, is killed 157 // before the save has completed (and when we know the id of the newly created message). When 158 // a save is completed, the service that is running in the background, will update the map 159 // 160 // When a new ComposeActivity instance is created, it will attempt to use the information in 161 // the previously instantiated map. If ComposeActivity.onCreate() is called, with a bundle 162 // (restoring data from a previous instance), and the map hasn't been created, we will attempt 163 // to populate the map with data stored in shared preferences. 164 // FIXME: values in this map are never read. 165 private static ConcurrentHashMap<Integer, Long> sRequestMessageIdMap = null; 166 // Key used to store the above map 167 private static final String CACHED_MESSAGE_REQUEST_IDS_KEY = "cache-message-request-ids"; 168 /** 169 * Notifies the {@code Activity} that the caller is an Email 170 * {@code Activity}, so that the back behavior may be modified accordingly. 171 * 172 * @see #onAppUpPressed 173 */ 174 public static final String EXTRA_FROM_EMAIL_TASK = "fromemail"; 175 176 public static final String EXTRA_ATTACHMENTS = "attachments"; 177 178 // If this is a reply/forward then this extra will hold the original message 179 private static final String EXTRA_IN_REFERENCE_TO_MESSAGE = "in-reference-to-message"; 180 // If this is a reply/forward then this extra will hold a uri we must query 181 // to get the original message. 182 protected static final String EXTRA_IN_REFERENCE_TO_MESSAGE_URI = "in-reference-to-message-uri"; 183 // If this is an action to edit an existing draft message, this extra will hold the 184 // draft message 185 private static final String ORIGINAL_DRAFT_MESSAGE = "original-draft-message"; 186 private static final String END_TOKEN = ", "; 187 private static final String LOG_TAG = LogTag.getLogTag(); 188 // Request numbers for activities we start 189 private static final int RESULT_PICK_ATTACHMENT = 1; 190 private static final int RESULT_CREATE_ACCOUNT = 2; 191 // TODO(mindyp) set mime-type for auto send? 192 public static final String AUTO_SEND_ACTION = "com.android.mail.action.AUTO_SEND"; 193 194 private static final String EXTRA_SELECTED_REPLY_FROM_ACCOUNT = "replyFromAccount"; 195 private static final String EXTRA_REQUEST_ID = "requestId"; 196 private static final String EXTRA_FOCUS_SELECTION_START = "focusSelectionStart"; 197 private static final String EXTRA_FOCUS_SELECTION_END = null; 198 private static final String EXTRA_MESSAGE = "extraMessage"; 199 private static final int REFERENCE_MESSAGE_LOADER = 0; 200 private static final int LOADER_ACCOUNT_CURSOR = 1; 201 private static final String EXTRA_SELECTED_ACCOUNT = "selectedAccount"; 202 private static final String TAG_WAIT = "wait-fragment"; 203 private static final String MIME_TYPE_PHOTO = "image/*"; 204 private static final String MIME_TYPE_VIDEO = "video/*"; 205 206 /** 207 * A single thread for running tasks in the background. 208 */ 209 private Handler mSendSaveTaskHandler = null; 210 private RecipientEditTextView mTo; 211 private RecipientEditTextView mCc; 212 private RecipientEditTextView mBcc; 213 private Button mCcBccButton; 214 private CcBccView mCcBccView; 215 private AttachmentsView mAttachmentsView; 216 private Account mAccount; 217 private ReplyFromAccount mReplyFromAccount; 218 private Settings mCachedSettings; 219 private Rfc822Validator mValidator; 220 private TextView mSubject; 221 222 private ComposeModeAdapter mComposeModeAdapter; 223 private int mComposeMode = -1; 224 private boolean mForward; 225 private String mRecipient; 226 private QuotedTextView mQuotedTextView; 227 private EditText mBodyView; 228 private View mFromStatic; 229 private TextView mFromStaticText; 230 private View mFromSpinnerWrapper; 231 @VisibleForTesting 232 protected FromAddressSpinner mFromSpinner; 233 private boolean mAddingAttachment; 234 private boolean mAttachmentsChanged; 235 private boolean mTextChanged; 236 private boolean mReplyFromChanged; 237 private MenuItem mSave; 238 private MenuItem mSend; 239 private AlertDialog mRecipientErrorDialog; 240 private AlertDialog mSendConfirmDialog; 241 @VisibleForTesting 242 protected Message mRefMessage; 243 private long mDraftId = UIProvider.INVALID_MESSAGE_ID; 244 private Message mDraft; 245 private ReplyFromAccount mDraftAccount; 246 private Object mDraftLock = new Object(); 247 private View mPhotoAttachmentsButton; 248 private View mVideoAttachmentsButton; 249 250 /** 251 * Boolean indicating whether ComposeActivity was launched from a Gmail controlled view. 252 */ 253 private boolean mLaunchedFromEmail = false; 254 private RecipientTextWatcher mToListener; 255 private RecipientTextWatcher mCcListener; 256 private RecipientTextWatcher mBccListener; 257 private Uri mRefMessageUri; 258 private Bundle mSavedInstanceState; 259 260 261 // Array of the outstanding send or save tasks. Access is synchronized 262 // with the object itself 263 /* package for testing */ 264 @VisibleForTesting 265 public ArrayList<SendOrSaveTask> mActiveTasks = Lists.newArrayList(); 266 // FIXME: this variable is never read. related to sRequestMessageIdMap. 267 private int mRequestId; 268 private String mSignature; 269 private Account[] mAccounts; 270 private boolean mRespondedInline; 271 272 /** 273 * Can be called from a non-UI thread. 274 */ 275 public static void editDraft(Context launcher, Account account, Message message) { 276 launch(launcher, account, message, EDIT_DRAFT); 277 } 278 279 /** 280 * Can be called from a non-UI thread. 281 */ 282 public static void compose(Context launcher, Account account) { 283 launch(launcher, account, null, COMPOSE); 284 } 285 286 /** 287 * Can be called from a non-UI thread. 288 */ 289 public static Intent createReplyIntent(final Context launcher, final Account account, 290 final Uri messageUri, final boolean isReplyAll) { 291 return createActionIntent(launcher, account, messageUri, isReplyAll ? REPLY_ALL : REPLY); 292 } 293 294 /** 295 * Can be called from a non-UI thread. 296 */ 297 public static Intent createForwardIntent(final Context launcher, final Account account, 298 final Uri messageUri) { 299 return createActionIntent(launcher, account, messageUri, FORWARD); 300 } 301 302 private static Intent createActionIntent(final Context launcher, final Account account, 303 final Uri messageUri, final int action) { 304 final Intent intent = new Intent(launcher, ComposeActivity.class); 305 306 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true); 307 intent.putExtra(EXTRA_ACTION, action); 308 intent.putExtra(Utils.EXTRA_ACCOUNT, account); 309 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI, messageUri); 310 311 return intent; 312 } 313 314 /** 315 * Can be called from a non-UI thread. 316 */ 317 public static void reply(Context launcher, Account account, Message message) { 318 launch(launcher, account, message, REPLY); 319 } 320 321 /** 322 * Can be called from a non-UI thread. 323 */ 324 public static void replyAll(Context launcher, Account account, Message message) { 325 launch(launcher, account, message, REPLY_ALL); 326 } 327 328 /** 329 * Can be called from a non-UI thread. 330 */ 331 public static void forward(Context launcher, Account account, Message message) { 332 launch(launcher, account, message, FORWARD); 333 } 334 335 private static void launch(Context launcher, Account account, Message message, int action) { 336 Intent intent = new Intent(launcher, ComposeActivity.class); 337 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true); 338 intent.putExtra(EXTRA_ACTION, action); 339 intent.putExtra(Utils.EXTRA_ACCOUNT, account); 340 if (action == EDIT_DRAFT) { 341 intent.putExtra(ORIGINAL_DRAFT_MESSAGE, message); 342 } else { 343 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE, message); 344 } 345 launcher.startActivity(intent); 346 } 347 348 @Override 349 public void onCreate(Bundle savedInstanceState) { 350 super.onCreate(savedInstanceState); 351 setContentView(R.layout.compose); 352 mSavedInstanceState = savedInstanceState; 353 checkValidAccounts(); 354 } 355 356 private void finishCreate() { 357 Bundle savedInstanceState = mSavedInstanceState; 358 findViews(); 359 Intent intent = getIntent(); 360 Message message; 361 ArrayList<AttachmentPreview> previews; 362 boolean showQuotedText = false; 363 int action; 364 // Check for any of the possibly supplied accounts.; 365 Account account = null; 366 if (hadSavedInstanceStateMessage(savedInstanceState)) { 367 action = savedInstanceState.getInt(EXTRA_ACTION, COMPOSE); 368 account = savedInstanceState.getParcelable(Utils.EXTRA_ACCOUNT); 369 message = (Message) savedInstanceState.getParcelable(EXTRA_MESSAGE); 370 371 previews = savedInstanceState.getParcelableArrayList(EXTRA_ATTACHMENT_PREVIEWS); 372 mRefMessage = (Message) savedInstanceState.getParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE); 373 } else { 374 account = obtainAccount(intent); 375 action = intent.getIntExtra(EXTRA_ACTION, COMPOSE); 376 // Initialize the message from the message in the intent 377 message = (Message) intent.getParcelableExtra(ORIGINAL_DRAFT_MESSAGE); 378 previews = intent.getParcelableArrayListExtra(EXTRA_ATTACHMENT_PREVIEWS); 379 mRefMessage = (Message) intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE); 380 mRefMessageUri = (Uri) intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI); 381 } 382 mAttachmentsView.setAttachmentPreviews(previews); 383 384 setAccount(account); 385 if (mAccount == null) { 386 return; 387 } 388 389 if (intent.getBooleanExtra(EXTRA_FROM_EMAIL_TASK, false)) { 390 mLaunchedFromEmail = true; 391 } else if (Intent.ACTION_SEND.equals(intent.getAction())) { 392 final Uri dataUri = intent.getData(); 393 if (dataUri != null) { 394 final String dataScheme = intent.getData().getScheme(); 395 final String accountScheme = mAccount.composeIntentUri.getScheme(); 396 mLaunchedFromEmail = TextUtils.equals(dataScheme, accountScheme); 397 } 398 } 399 400 if (mRefMessageUri != null) { 401 // We have a referenced message that we must look up. 402 getLoaderManager().initLoader(REFERENCE_MESSAGE_LOADER, null, this); 403 return; 404 } else if (message != null && action != EDIT_DRAFT) { 405 initFromDraftMessage(message); 406 initQuotedTextFromRefMessage(mRefMessage, action); 407 showCcBcc(savedInstanceState); 408 showQuotedText = message.appendRefMessageContent; 409 } else if (action == EDIT_DRAFT) { 410 initFromDraftMessage(message); 411 boolean showBcc = !TextUtils.isEmpty(message.getBcc()); 412 boolean showCc = showBcc || !TextUtils.isEmpty(message.getCc()); 413 mCcBccView.show(false, showCc, showBcc); 414 // Update the action to the draft type of the previous draft 415 switch (message.draftType) { 416 case UIProvider.DraftType.REPLY: 417 action = REPLY; 418 break; 419 case UIProvider.DraftType.REPLY_ALL: 420 action = REPLY_ALL; 421 break; 422 case UIProvider.DraftType.FORWARD: 423 action = FORWARD; 424 break; 425 case UIProvider.DraftType.COMPOSE: 426 default: 427 action = COMPOSE; 428 break; 429 } 430 initQuotedTextFromRefMessage(mRefMessage, action); 431 showQuotedText = message.appendRefMessageContent; 432 } else if ((action == REPLY || action == REPLY_ALL || action == FORWARD)) { 433 if (mRefMessage != null) { 434 initFromRefMessage(action); 435 showQuotedText = true; 436 } 437 } else { 438 initFromExtras(intent); 439 } 440 finishSetup(action, intent, savedInstanceState, showQuotedText); 441 } 442 443 private void checkValidAccounts() { 444 final Account[] allAccounts = AccountUtils.getAccounts(this); 445 if (allAccounts == null || allAccounts.length == 0) { 446 final Intent noAccountIntent = MailAppProvider.getNoAccountIntent(this); 447 if (noAccountIntent != null) { 448 mAccounts = null; 449 startActivityForResult(noAccountIntent, RESULT_CREATE_ACCOUNT); 450 } 451 } else { 452 // If none of the accounts are syncing, setup a watcher. 453 boolean anySyncing = false; 454 for (Account a : allAccounts) { 455 if (a.isAccountReady()) { 456 anySyncing = true; 457 break; 458 } 459 } 460 if (!anySyncing) { 461 // There are accounts, but none are sync'd, which is just like having no accounts. 462 mAccounts = null; 463 getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this); 464 return; 465 } 466 mAccounts = AccountUtils.getSyncingAccounts(this); 467 finishCreate(); 468 } 469 } 470 471 private Account obtainAccount(Intent intent) { 472 Account account = null; 473 Object accountExtra = null; 474 if (intent != null && intent.getExtras() != null) { 475 accountExtra = intent.getExtras().get(Utils.EXTRA_ACCOUNT); 476 if (accountExtra instanceof Account) { 477 return (Account) accountExtra; 478 } else if (accountExtra instanceof String) { 479 // This is the Account attached to the widget compose intent. 480 account = Account.newinstance((String)accountExtra); 481 if (account != null) { 482 return account; 483 } 484 } 485 accountExtra = intent.getStringExtra(EXTRA_SELECTED_ACCOUNT); 486 } 487 if (account == null) { 488 MailAppProvider provider = MailAppProvider.getInstance(); 489 String lastAccountUri = provider.getLastSentFromAccount(); 490 if (TextUtils.isEmpty(lastAccountUri)) { 491 lastAccountUri = provider.getLastViewedAccount(); 492 } 493 if (!TextUtils.isEmpty(lastAccountUri)) { 494 accountExtra = Uri.parse(lastAccountUri); 495 } 496 } 497 if (mAccounts != null && mAccounts.length > 0) { 498 if (accountExtra instanceof String && !TextUtils.isEmpty((String) accountExtra)) { 499 // For backwards compatibility, we need to check account 500 // names. 501 for (Account a : mAccounts) { 502 if (a.name.equals(accountExtra)) { 503 account = a; 504 } 505 } 506 } else if (accountExtra instanceof Uri) { 507 // The uri of the last viewed account is what is stored in 508 // the current code base. 509 for (Account a : mAccounts) { 510 if (a.uri.equals(accountExtra)) { 511 account = a; 512 } 513 } 514 } 515 if (account == null) { 516 account = mAccounts[0]; 517 } 518 } 519 return account; 520 } 521 522 private void finishSetup(int action, Intent intent, Bundle savedInstanceState, 523 boolean showQuotedText) { 524 setFocus(action); 525 if (action == COMPOSE) { 526 mQuotedTextView.setVisibility(View.GONE); 527 } 528 initRecipients(); 529 // Don't bother with the intent if we have procured a message from the 530 // intent already. 531 if (!hadSavedInstanceStateMessage(savedInstanceState)) { 532 initAttachmentsFromIntent(intent); 533 } 534 initActionBar(action); 535 initFromSpinner(savedInstanceState != null ? savedInstanceState : intent.getExtras(), 536 action); 537 538 // If this is a draft message, the draft account is whatever account was 539 // used to open the draft message in Compose. 540 if (mDraft != null) { 541 mDraftAccount = mReplyFromAccount; 542 } 543 544 initChangeListeners(); 545 updateHideOrShowCcBcc(); 546 updateHideOrShowQuotedText(showQuotedText); 547 548 mRespondedInline = mSavedInstanceState != null ? 549 mSavedInstanceState.getBoolean(EXTRA_RESPONDED_INLINE) : false; 550 if (mRespondedInline) { 551 mQuotedTextView.setVisibility(View.GONE); 552 } 553 } 554 555 private boolean hadSavedInstanceStateMessage(Bundle savedInstanceState) { 556 return savedInstanceState != null && savedInstanceState.containsKey(EXTRA_MESSAGE); 557 } 558 559 private void updateHideOrShowQuotedText(boolean showQuotedText) { 560 mQuotedTextView.updateCheckedState(showQuotedText); 561 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0); 562 } 563 564 private void setFocus(int action) { 565 if (action == EDIT_DRAFT) { 566 int type = mDraft.draftType; 567 switch (type) { 568 case UIProvider.DraftType.COMPOSE: 569 case UIProvider.DraftType.FORWARD: 570 action = COMPOSE; 571 break; 572 case UIProvider.DraftType.REPLY: 573 case UIProvider.DraftType.REPLY_ALL: 574 default: 575 action = REPLY; 576 break; 577 } 578 } 579 switch (action) { 580 case FORWARD: 581 case COMPOSE: 582 if (TextUtils.isEmpty(mTo.getText())) { 583 mTo.requestFocus(); 584 break; 585 } 586 case REPLY: 587 case REPLY_ALL: 588 default: 589 focusBody(); 590 break; 591 } 592 } 593 594 /** 595 * Focus the body of the message. 596 */ 597 public void focusBody() { 598 mBodyView.requestFocus(); 599 int length = mBodyView.getText().length(); 600 601 int signatureStartPos = getSignatureStartPosition( 602 mSignature, mBodyView.getText().toString()); 603 if (signatureStartPos > -1) { 604 // In case the user deleted the newlines... 605 mBodyView.setSelection(signatureStartPos); 606 } else if (length >= 0) { 607 // Move cursor to the end. 608 mBodyView.setSelection(length); 609 } 610 } 611 612 @Override 613 protected void onResume() { 614 super.onResume(); 615 // Update the from spinner as other accounts 616 // may now be available. 617 if (mFromSpinner != null && mAccount != null) { 618 mFromSpinner.asyncInitFromSpinner(mComposeMode, mAccount, mAccounts); 619 } 620 } 621 622 @Override 623 protected void onPause() { 624 super.onPause(); 625 626 if (mSendConfirmDialog != null) { 627 mSendConfirmDialog.dismiss(); 628 } 629 if (mRecipientErrorDialog != null) { 630 mRecipientErrorDialog.dismiss(); 631 } 632 // When the user exits the compose view, see if this draft needs saving. 633 // Don't save unnecessary drafts if we are only changing the orientation. 634 if (!isChangingConfigurations()) { 635 saveIfNeeded(); 636 } 637 } 638 639 @Override 640 protected final void onActivityResult(int request, int result, Intent data) { 641 if (request == RESULT_PICK_ATTACHMENT && result == RESULT_OK) { 642 addAttachmentAndUpdateView(data); 643 mAddingAttachment = false; 644 } else if (request == RESULT_CREATE_ACCOUNT) { 645 // We were waiting for the user to create an account 646 if (result != RESULT_OK) { 647 finish(); 648 } else { 649 // Watch for accounts to show up! 650 // restart the loader to get the updated list of accounts 651 getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this); 652 showWaitFragment(null); 653 } 654 } 655 } 656 657 @Override 658 public final void onRestoreInstanceState(Bundle savedInstanceState) { 659 clearChangeListeners(); 660 super.onRestoreInstanceState(savedInstanceState); 661 if (savedInstanceState != null) { 662 if (savedInstanceState.containsKey(EXTRA_FOCUS_SELECTION_START)) { 663 int selectionStart = savedInstanceState.getInt(EXTRA_FOCUS_SELECTION_START); 664 int selectionEnd = savedInstanceState.getInt(EXTRA_FOCUS_SELECTION_END); 665 // There should be a focus and it should be an EditText since we 666 // only save these extras if these conditions are true. 667 EditText focusEditText = (EditText) getCurrentFocus(); 668 final int length = focusEditText.getText().length(); 669 if (selectionStart < length && selectionEnd < length) { 670 focusEditText.setSelection(selectionStart, selectionEnd); 671 } 672 } 673 } 674 initChangeListeners(); 675 } 676 677 @Override 678 public final void onSaveInstanceState(Bundle state) { 679 super.onSaveInstanceState(state); 680 // We have no accounts so there is nothing to compose, and therefore, nothing to save. 681 if (mAccounts == null || mAccounts.length == 0) { 682 return; 683 } 684 // The framework is happy to save and restore the selection but only if it also saves and 685 // restores the contents of the edit text. That's a lot of text to put in a bundle so we do 686 // this manually. 687 View focus = getCurrentFocus(); 688 if (focus != null && focus instanceof EditText) { 689 EditText focusEditText = (EditText) focus; 690 state.putInt(EXTRA_FOCUS_SELECTION_START, focusEditText.getSelectionStart()); 691 state.putInt(EXTRA_FOCUS_SELECTION_END, focusEditText.getSelectionEnd()); 692 } 693 694 final List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts(); 695 final int selectedPos = mFromSpinner.getSelectedItemPosition(); 696 final ReplyFromAccount selectedReplyFromAccount = (replyFromAccounts != null 697 && replyFromAccounts.size() > 0 && replyFromAccounts.size() > selectedPos) ? 698 replyFromAccounts.get(selectedPos) : null; 699 if (selectedReplyFromAccount != null) { 700 state.putString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT, selectedReplyFromAccount.serialize() 701 .toString()); 702 state.putParcelable(Utils.EXTRA_ACCOUNT, selectedReplyFromAccount.account); 703 } else { 704 state.putParcelable(Utils.EXTRA_ACCOUNT, mAccount); 705 } 706 707 if (mDraftId == UIProvider.INVALID_MESSAGE_ID && mRequestId !=0) { 708 // We don't have a draft id, and we have a request id, 709 // save the request id. 710 state.putInt(EXTRA_REQUEST_ID, mRequestId); 711 } 712 713 // We want to restore the current mode after a pause 714 // or rotation. 715 int mode = getMode(); 716 state.putInt(EXTRA_ACTION, mode); 717 718 final Message message = createMessage(selectedReplyFromAccount, mode); 719 if (mDraft != null) { 720 message.id = mDraft.id; 721 message.serverId = mDraft.serverId; 722 message.uri = mDraft.uri; 723 } 724 state.putParcelable(EXTRA_MESSAGE, message); 725 726 if (mRefMessage != null) { 727 state.putParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE, mRefMessage); 728 } 729 state.putBoolean(EXTRA_SHOW_CC, mCcBccView.isCcVisible()); 730 state.putBoolean(EXTRA_SHOW_BCC, mCcBccView.isBccVisible()); 731 state.putBoolean(EXTRA_RESPONDED_INLINE, mRespondedInline); 732 state.putBoolean(EXTRA_SAVE_ENABLED, mSave != null && mSave.isEnabled()); 733 state.putParcelableArrayList( 734 EXTRA_ATTACHMENT_PREVIEWS, mAttachmentsView.getAttachmentPreviews()); 735 } 736 737 private int getMode() { 738 int mode = ComposeActivity.COMPOSE; 739 ActionBar actionBar = getActionBar(); 740 if (actionBar != null 741 && actionBar.getNavigationMode() == ActionBar.NAVIGATION_MODE_LIST) { 742 mode = actionBar.getSelectedNavigationIndex(); 743 } 744 return mode; 745 } 746 747 private Message createMessage(ReplyFromAccount selectedReplyFromAccount, int mode) { 748 Message message = new Message(); 749 message.id = UIProvider.INVALID_MESSAGE_ID; 750 message.serverId = null; 751 message.uri = null; 752 message.conversationUri = null; 753 message.subject = mSubject.getText().toString(); 754 message.snippet = null; 755 message.setTo(formatSenders(mTo.getText().toString())); 756 message.setCc(formatSenders(mCc.getText().toString())); 757 message.setBcc(formatSenders(mBcc.getText().toString())); 758 message.setReplyTo(null); 759 message.dateReceivedMs = 0; 760 String htmlBody = Html.toHtml(new SpannableString(mBodyView.getText().toString())); 761 StringBuilder fullBody = new StringBuilder(htmlBody); 762 message.bodyHtml = fullBody.toString(); 763 message.bodyText = mBodyView.getText().toString(); 764 message.embedsExternalResources = false; 765 message.refMessageId = mRefMessage != null ? mRefMessage.uri.toString() : null; 766 message.appendRefMessageContent = mQuotedTextView.getQuotedTextIfIncluded() != null; 767 ArrayList<Attachment> attachments = mAttachmentsView.getAttachments(); 768 message.hasAttachments = attachments != null && attachments.size() > 0; 769 message.attachmentListUri = null; 770 message.messageFlags = 0; 771 message.saveUri = null; 772 message.sendUri = null; 773 message.alwaysShowImages = false; 774 message.attachmentsJson = Attachment.toJSONArray(attachments); 775 CharSequence quotedText = mQuotedTextView.getQuotedText(); 776 message.quotedTextOffset = !TextUtils.isEmpty(quotedText) ? QuotedTextView 777 .getQuotedTextOffset(quotedText.toString()) : -1; 778 message.accountUri = null; 779 message.setFrom(selectedReplyFromAccount != null ? selectedReplyFromAccount.address 780 : mAccount != null ? mAccount.name : null); 781 message.draftType = getDraftType(mode); 782 message.setTo(formatSenders(mTo.getText().toString())); 783 message.setCc(formatSenders(mCc.getText().toString())); 784 message.setBcc(formatSenders(mBcc.getText().toString())); 785 return message; 786 } 787 788 private String formatSenders(String string) { 789 if (!TextUtils.isEmpty(string) && string.charAt(string.length() - 1) == ',') { 790 return string.substring(0, string.length() - 1); 791 } 792 return string; 793 } 794 795 @VisibleForTesting 796 void setAccount(Account account) { 797 if (account == null) { 798 return; 799 } 800 if (!account.equals(mAccount)) { 801 mAccount = account; 802 mCachedSettings = mAccount.settings; 803 appendSignature(); 804 } 805 if (mAccount != null) { 806 MailActivity.setNfcMessage(mAccount.name); 807 } 808 } 809 810 private void initFromSpinner(Bundle bundle, int action) { 811 String accountString = null; 812 if (action == EDIT_DRAFT && mDraft.draftType == UIProvider.DraftType.COMPOSE) { 813 action = COMPOSE; 814 } 815 mFromSpinner.asyncInitFromSpinner(action, mAccount, mAccounts); 816 if (bundle != null) { 817 if (bundle.containsKey(EXTRA_SELECTED_REPLY_FROM_ACCOUNT)) { 818 mReplyFromAccount = ReplyFromAccount.deserialize(mAccount, 819 bundle.getString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT)); 820 } else if (bundle.containsKey(EXTRA_FROM_ACCOUNT_STRING)) { 821 accountString = bundle.getString(EXTRA_FROM_ACCOUNT_STRING); 822 mReplyFromAccount = mFromSpinner.getMatchingReplyFromAccount(accountString); 823 } 824 } 825 if (mReplyFromAccount == null) { 826 if (mDraft != null) { 827 mReplyFromAccount = getReplyFromAccountFromDraft(mAccount, mDraft); 828 } else if (mRefMessage != null) { 829 mReplyFromAccount = getReplyFromAccountForReply(mAccount, mRefMessage); 830 } 831 } 832 if (mReplyFromAccount == null) { 833 mReplyFromAccount = getDefaultReplyFromAccount(mAccount); 834 } 835 836 mFromSpinner.setCurrentAccount(mReplyFromAccount); 837 838 if (mFromSpinner.getCount() > 1) { 839 // If there is only 1 account, just show that account. 840 // Otherwise, give the user the ability to choose which account to 841 // send mail from / save drafts to. 842 mFromStatic.setVisibility(View.GONE); 843 mFromStaticText.setText(mAccount.name); 844 mFromSpinnerWrapper.setVisibility(View.VISIBLE); 845 } else { 846 mFromStatic.setVisibility(View.VISIBLE); 847 mFromStaticText.setText(mAccount.name); 848 mFromSpinnerWrapper.setVisibility(View.GONE); 849 } 850 } 851 852 private ReplyFromAccount getReplyFromAccountForReply(Account account, Message refMessage) { 853 if (refMessage.accountUri != null) { 854 // This must be from combined inbox. 855 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts(); 856 for (ReplyFromAccount from : replyFromAccounts) { 857 if (from.account.uri.equals(refMessage.accountUri)) { 858 return from; 859 } 860 } 861 return null; 862 } else { 863 return getReplyFromAccount(account, refMessage); 864 } 865 } 866 867 /** 868 * Given an account and which email address the message was sent to, 869 * return who the message should be sent from. 870 * @param account Account in which the message arrived. 871 * @param sentTo Email address to which the message was sent. 872 * @return the address from which to reply. 873 */ 874 public ReplyFromAccount getReplyFromAccount(Account account, Message refMessage) { 875 // First see if we are supposed to use the default address or 876 // the address it was sentTo. 877 if (mCachedSettings.forceReplyFromDefault) { 878 return getDefaultReplyFromAccount(account); 879 } else { 880 // If we aren't explicitly told which account to look for, look at 881 // all the message recipients and find one that matches 882 // a custom from or account. 883 List<String> allRecipients = new ArrayList<String>(); 884 allRecipients.addAll(Arrays.asList(refMessage.getToAddresses())); 885 allRecipients.addAll(Arrays.asList(refMessage.getCcAddresses())); 886 return getMatchingRecipient(account, allRecipients); 887 } 888 } 889 890 /** 891 * Compare all the recipients of an email to the current account and all 892 * custom addresses associated with that account. Return the match if there 893 * is one, or the default account if there isn't. 894 */ 895 protected ReplyFromAccount getMatchingRecipient(Account account, List<String> sentTo) { 896 // Tokenize the list and place in a hashmap. 897 ReplyFromAccount matchingReplyFrom = null; 898 Rfc822Token[] tokens; 899 HashSet<String> recipientsMap = new HashSet<String>(); 900 for (String address : sentTo) { 901 tokens = Rfc822Tokenizer.tokenize(address); 902 for (int i = 0; i < tokens.length; i++) { 903 recipientsMap.add(tokens[i].getAddress()); 904 } 905 } 906 907 int matchingAddressCount = 0; 908 List<ReplyFromAccount> customFroms; 909 customFroms = account.getReplyFroms(); 910 if (customFroms != null) { 911 for (ReplyFromAccount entry : customFroms) { 912 if (recipientsMap.contains(entry.address)) { 913 matchingReplyFrom = entry; 914 matchingAddressCount++; 915 } 916 } 917 } 918 if (matchingAddressCount > 1) { 919 matchingReplyFrom = getDefaultReplyFromAccount(account); 920 } 921 return matchingReplyFrom; 922 } 923 924 private ReplyFromAccount getDefaultReplyFromAccount(Account account) { 925 for (ReplyFromAccount from : account.getReplyFroms()) { 926 if (from.isDefault) { 927 return from; 928 } 929 } 930 return new ReplyFromAccount(account, account.uri, account.name, account.name, account.name, 931 true, false); 932 } 933 934 private ReplyFromAccount getReplyFromAccountFromDraft(Account account, Message msg) { 935 String sender = msg.getFrom(); 936 ReplyFromAccount replyFromAccount = null; 937 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts(); 938 if (TextUtils.equals(account.name, sender)) { 939 replyFromAccount = new ReplyFromAccount(mAccount, mAccount.uri, mAccount.name, 940 mAccount.name, mAccount.name, true, false); 941 } else { 942 for (ReplyFromAccount fromAccount : replyFromAccounts) { 943 if (TextUtils.equals(fromAccount.name, sender)) { 944 replyFromAccount = fromAccount; 945 break; 946 } 947 } 948 } 949 return replyFromAccount; 950 } 951 952 private void findViews() { 953 findViewById(R.id.compose).setVisibility(View.VISIBLE); 954 mCcBccButton = (Button) findViewById(R.id.add_cc_bcc); 955 if (mCcBccButton != null) { 956 mCcBccButton.setOnClickListener(this); 957 } 958 mCcBccView = (CcBccView) findViewById(R.id.cc_bcc_wrapper); 959 mAttachmentsView = (AttachmentsView)findViewById(R.id.attachments); 960 mPhotoAttachmentsButton = findViewById(R.id.add_photo_attachment); 961 if (mPhotoAttachmentsButton != null) { 962 mPhotoAttachmentsButton.setOnClickListener(this); 963 } 964 mVideoAttachmentsButton = findViewById(R.id.add_video_attachment); 965 if (mVideoAttachmentsButton != null) { 966 mVideoAttachmentsButton.setOnClickListener(this); 967 } 968 mTo = (RecipientEditTextView) findViewById(R.id.to); 969 mCc = (RecipientEditTextView) findViewById(R.id.cc); 970 mBcc = (RecipientEditTextView) findViewById(R.id.bcc); 971 // TODO: add special chips text change watchers before adding 972 // this as a text changed watcher to the to, cc, bcc fields. 973 mSubject = (TextView) findViewById(R.id.subject); 974 mSubject.setOnEditorActionListener(this); 975 mQuotedTextView = (QuotedTextView) findViewById(R.id.quoted_text_view); 976 mQuotedTextView.setRespondInlineListener(this); 977 mBodyView = (EditText) findViewById(R.id.body); 978 mFromStatic = findViewById(R.id.static_from_content); 979 mFromStaticText = (TextView) findViewById(R.id.from_account_name); 980 mFromSpinnerWrapper = findViewById(R.id.spinner_from_content); 981 mFromSpinner = (FromAddressSpinner) findViewById(R.id.from_picker); 982 } 983 984 @Override 985 public boolean onEditorAction(TextView view, int action, KeyEvent keyEvent) { 986 if (action == EditorInfo.IME_ACTION_DONE) { 987 focusBody(); 988 return true; 989 } 990 return false; 991 } 992 993 protected TextView getBody() { 994 return mBodyView; 995 } 996 997 @VisibleForTesting 998 public Account getFromAccount() { 999 return mReplyFromAccount != null && mReplyFromAccount.account != null ? 1000 mReplyFromAccount.account : mAccount; 1001 } 1002 1003 private void clearChangeListeners() { 1004 mSubject.removeTextChangedListener(this); 1005 mBodyView.removeTextChangedListener(this); 1006 mTo.removeTextChangedListener(mToListener); 1007 mCc.removeTextChangedListener(mCcListener); 1008 mBcc.removeTextChangedListener(mBccListener); 1009 mFromSpinner.setOnAccountChangedListener(null); 1010 mAttachmentsView.setAttachmentChangesListener(null); 1011 } 1012 1013 // Now that the message has been initialized from any existing draft or 1014 // ref message data, set up listeners for any changes that occur to the 1015 // message. 1016 private void initChangeListeners() { 1017 // Make sure we only add text changed listeners once! 1018 clearChangeListeners(); 1019 mSubject.addTextChangedListener(this); 1020 mBodyView.addTextChangedListener(this); 1021 if (mToListener == null) { 1022 mToListener = new RecipientTextWatcher(mTo, this); 1023 } 1024 mTo.addTextChangedListener(mToListener); 1025 if (mCcListener == null) { 1026 mCcListener = new RecipientTextWatcher(mCc, this); 1027 } 1028 mCc.addTextChangedListener(mCcListener); 1029 if (mBccListener == null) { 1030 mBccListener = new RecipientTextWatcher(mBcc, this); 1031 } 1032 mBcc.addTextChangedListener(mBccListener); 1033 mFromSpinner.setOnAccountChangedListener(this); 1034 mAttachmentsView.setAttachmentChangesListener(this); 1035 } 1036 1037 private void initActionBar(int action) { 1038 mComposeMode = action; 1039 ActionBar actionBar = getActionBar(); 1040 if (actionBar == null) { 1041 return; 1042 } 1043 if (action == ComposeActivity.COMPOSE) { 1044 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD); 1045 actionBar.setTitle(R.string.compose); 1046 } else { 1047 actionBar.setTitle(null); 1048 if (mComposeModeAdapter == null) { 1049 mComposeModeAdapter = new ComposeModeAdapter(this); 1050 } 1051 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST); 1052 actionBar.setListNavigationCallbacks(mComposeModeAdapter, this); 1053 switch (action) { 1054 case ComposeActivity.REPLY: 1055 actionBar.setSelectedNavigationItem(0); 1056 break; 1057 case ComposeActivity.REPLY_ALL: 1058 actionBar.setSelectedNavigationItem(1); 1059 break; 1060 case ComposeActivity.FORWARD: 1061 actionBar.setSelectedNavigationItem(2); 1062 break; 1063 } 1064 } 1065 actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME, 1066 ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME); 1067 actionBar.setHomeButtonEnabled(true); 1068 } 1069 1070 private void initFromRefMessage(int action) { 1071 setFieldsFromRefMessage(action); 1072 if (mRefMessage != null) { 1073 // CC field only gets populated when doing REPLY_ALL. 1074 // BCC never gets auto-populated, unless the user is editing 1075 // a draft with one. 1076 if (!TextUtils.isEmpty(mCc.getText()) && action == REPLY_ALL) { 1077 mCcBccView.show(false, true, false); 1078 } 1079 } 1080 updateHideOrShowCcBcc(); 1081 } 1082 1083 private void setFieldsFromRefMessage(int action) { 1084 setSubject(mRefMessage, action); 1085 // Setup recipients 1086 if (action == FORWARD) { 1087 mForward = true; 1088 } 1089 initRecipientsFromRefMessage(mRefMessage, action); 1090 initQuotedTextFromRefMessage(mRefMessage, action); 1091 if (action == ComposeActivity.FORWARD || mAttachmentsChanged) { 1092 initAttachments(mRefMessage); 1093 } 1094 } 1095 1096 private void initFromDraftMessage(Message message) { 1097 LogUtils.d(LOG_TAG, "Intializing draft from previous draft message: %s", message); 1098 1099 mDraft = message; 1100 mDraftId = message.id; 1101 mSubject.setText(message.subject); 1102 mForward = message.draftType == UIProvider.DraftType.FORWARD; 1103 final List<String> toAddresses = Arrays.asList(message.getToAddresses()); 1104 addToAddresses(toAddresses); 1105 addCcAddresses(Arrays.asList(message.getCcAddresses()), toAddresses); 1106 addBccAddresses(Arrays.asList(message.getBccAddresses())); 1107 if (message.hasAttachments) { 1108 List<Attachment> attachments = message.getAttachments(); 1109 for (Attachment a : attachments) { 1110 addAttachmentAndUpdateView(a); 1111 } 1112 } 1113 int quotedTextIndex = message.appendRefMessageContent ? 1114 message.quotedTextOffset : -1; 1115 // Set the body 1116 CharSequence quotedText = null; 1117 if (!TextUtils.isEmpty(message.bodyHtml)) { 1118 CharSequence htmlText = ""; 1119 if (quotedTextIndex > -1) { 1120 // Find the offset in the htmltext of the actual quoted text and strip it out. 1121 quotedTextIndex = QuotedTextView.findQuotedTextIndex(message.bodyHtml); 1122 if (quotedTextIndex > -1) { 1123 htmlText = Utils.convertHtmlToPlainText(message.bodyHtml.substring(0, 1124 quotedTextIndex)); 1125 quotedText = message.bodyHtml.subSequence(quotedTextIndex, 1126 message.bodyHtml.length()); 1127 } 1128 } else { 1129 htmlText = Utils.convertHtmlToPlainText(message.bodyHtml); 1130 } 1131 mBodyView.setText(htmlText); 1132 } else { 1133 final String body = message.bodyText; 1134 final CharSequence bodyText = !TextUtils.isEmpty(body) ? 1135 (quotedTextIndex > -1 ? 1136 message.bodyText.substring(0, quotedTextIndex) : message.bodyText) 1137 : ""; 1138 if (quotedTextIndex > -1) { 1139 quotedText = !TextUtils.isEmpty(body) ? message.bodyText.substring(quotedTextIndex) 1140 : null; 1141 } 1142 mBodyView.setText(bodyText); 1143 } 1144 if (quotedTextIndex > -1 && quotedText != null) { 1145 mQuotedTextView.setQuotedTextFromDraft(quotedText, mForward); 1146 } 1147 } 1148 1149 /** 1150 * Fill all the widgets with the content found in the Intent Extra, if any. 1151 * Also apply the same style to all widgets. Note: if initFromExtras is 1152 * called as a result of switching between reply, reply all, and forward per 1153 * the latest revision of Gmail, and the user has already made changes to 1154 * attachments on a previous incarnation of the message (as a reply, reply 1155 * all, or forward), the original attachments from the message will not be 1156 * re-instantiated. The user's changes will be respected. This follows the 1157 * web gmail interaction. 1158 */ 1159 public void initFromExtras(Intent intent) { 1160 // If we were invoked with a SENDTO intent, the value 1161 // should take precedence 1162 final Uri dataUri = intent.getData(); 1163 if (dataUri != null) { 1164 if (MAIL_TO.equals(dataUri.getScheme())) { 1165 initFromMailTo(dataUri.toString()); 1166 } else { 1167 if (!mAccount.composeIntentUri.equals(dataUri)) { 1168 String toText = dataUri.getSchemeSpecificPart(); 1169 if (toText != null) { 1170 mTo.setText(""); 1171 addToAddresses(Arrays.asList(TextUtils.split(toText, ","))); 1172 } 1173 } 1174 } 1175 } 1176 1177 String[] extraStrings = intent.getStringArrayExtra(Intent.EXTRA_EMAIL); 1178 if (extraStrings != null) { 1179 addToAddresses(Arrays.asList(extraStrings)); 1180 } 1181 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_CC); 1182 if (extraStrings != null) { 1183 addCcAddresses(Arrays.asList(extraStrings), null); 1184 } 1185 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_BCC); 1186 if (extraStrings != null) { 1187 addBccAddresses(Arrays.asList(extraStrings)); 1188 } 1189 1190 String extraString = intent.getStringExtra(Intent.EXTRA_SUBJECT); 1191 if (extraString != null) { 1192 mSubject.setText(extraString); 1193 } 1194 1195 for (String extra : ALL_EXTRAS) { 1196 if (intent.hasExtra(extra)) { 1197 String value = intent.getStringExtra(extra); 1198 if (EXTRA_TO.equals(extra)) { 1199 addToAddresses(Arrays.asList(TextUtils.split(value, ","))); 1200 } else if (EXTRA_CC.equals(extra)) { 1201 addCcAddresses(Arrays.asList(TextUtils.split(value, ",")), null); 1202 } else if (EXTRA_BCC.equals(extra)) { 1203 addBccAddresses(Arrays.asList(TextUtils.split(value, ","))); 1204 } else if (EXTRA_SUBJECT.equals(extra)) { 1205 mSubject.setText(value); 1206 } else if (EXTRA_BODY.equals(extra)) { 1207 setBody(value, true /* with signature */); 1208 } 1209 } 1210 } 1211 1212 Bundle extras = intent.getExtras(); 1213 if (extras != null) { 1214 CharSequence text = extras.getCharSequence(Intent.EXTRA_TEXT); 1215 if (text != null) { 1216 setBody(text, true /* with signature */); 1217 } 1218 } 1219 } 1220 1221 @VisibleForTesting 1222 protected String decodeEmailInUri(String s) throws UnsupportedEncodingException { 1223 // TODO: handle the case where there are spaces in the display name as 1224 // well as the email such as "Guy with spaces <guy+with+spaces@gmail.com>" 1225 // as they could be encoded ambiguously. 1226 // Since URLDecode.decode changes + into ' ', and + is a valid 1227 // email character, we need to find/ replace these ourselves before 1228 // decoding. 1229 String replacePlus = s.replace("+", "%2B"); 1230 try { 1231 return URLDecoder.decode(replacePlus, UTF8_ENCODING_NAME); 1232 } catch (IllegalArgumentException e) { 1233 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) { 1234 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), s); 1235 } else { 1236 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address"); 1237 } 1238 return null; 1239 } 1240 } 1241 1242 /** 1243 * Initialize the compose view from a String representing a mailTo uri. 1244 * @param mailToString The uri as a string. 1245 */ 1246 public void initFromMailTo(String mailToString) { 1247 // We need to disguise this string as a URI in order to parse it 1248 // TODO: Remove this hack when http://b/issue?id=1445295 gets fixed 1249 Uri uri = Uri.parse("foo://" + mailToString); 1250 int index = mailToString.indexOf("?"); 1251 int length = "mailto".length() + 1; 1252 String to; 1253 try { 1254 // Extract the recipient after mailto: 1255 if (index == -1) { 1256 to = decodeEmailInUri(mailToString.substring(length)); 1257 } else { 1258 to = decodeEmailInUri(mailToString.substring(length, index)); 1259 } 1260 if (!TextUtils.isEmpty(to)) { 1261 addToAddresses(Arrays.asList(TextUtils.split(to, ","))); 1262 } 1263 } catch (UnsupportedEncodingException e) { 1264 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) { 1265 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), mailToString); 1266 } else { 1267 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address"); 1268 } 1269 } 1270 1271 List<String> cc = uri.getQueryParameters("cc"); 1272 addCcAddresses(Arrays.asList(cc.toArray(new String[cc.size()])), null); 1273 1274 List<String> otherTo = uri.getQueryParameters("to"); 1275 addToAddresses(Arrays.asList(otherTo.toArray(new String[otherTo.size()]))); 1276 1277 List<String> bcc = uri.getQueryParameters("bcc"); 1278 addBccAddresses(Arrays.asList(bcc.toArray(new String[bcc.size()]))); 1279 1280 List<String> subject = uri.getQueryParameters("subject"); 1281 if (subject.size() > 0) { 1282 try { 1283 mSubject.setText(URLDecoder.decode(subject.get(0), UTF8_ENCODING_NAME)); 1284 } catch (UnsupportedEncodingException e) { 1285 LogUtils.e(LOG_TAG, "%s while decoding subject '%s'", 1286 e.getMessage(), subject); 1287 } 1288 } 1289 1290 List<String> body = uri.getQueryParameters("body"); 1291 if (body.size() > 0) { 1292 try { 1293 setBody(URLDecoder.decode(body.get(0), UTF8_ENCODING_NAME), 1294 true /* with signature */); 1295 } catch (UnsupportedEncodingException e) { 1296 LogUtils.e(LOG_TAG, "%s while decoding body '%s'", e.getMessage(), body); 1297 } 1298 } 1299 } 1300 1301 @VisibleForTesting 1302 protected void initAttachments(Message refMessage) { 1303 addAttachments(refMessage.getAttachments()); 1304 } 1305 1306 public long addAttachments(List<Attachment> attachments) { 1307 long size = 0; 1308 AttachmentFailureException error = null; 1309 for (Attachment a : attachments) { 1310 try { 1311 size += mAttachmentsView.addAttachment(mAccount, a); 1312 } catch (AttachmentFailureException e) { 1313 error = e; 1314 } 1315 } 1316 if (error != null) { 1317 LogUtils.e(LOG_TAG, error, "Error adding attachment"); 1318 if (attachments.size() > 1) { 1319 showAttachmentTooBigToast(R.string.too_large_to_attach_multiple); 1320 } else { 1321 showAttachmentTooBigToast(error.getErrorRes()); 1322 } 1323 } 1324 return size; 1325 } 1326 1327 /** 1328 * When an attachment is too large to be added to a message, show a toast. 1329 * This method also updates the position of the toast so that it is shown 1330 * clearly above they keyboard if it happens to be open. 1331 */ 1332 private void showAttachmentTooBigToast(int errorRes) { 1333 String maxSize = AttachmentUtils.convertToHumanReadableSize( 1334 getApplicationContext(), mAccount.settings.getMaxAttachmentSize()); 1335 showErrorToast(getString(errorRes, maxSize)); 1336 } 1337 1338 private void showErrorToast(String message) { 1339 Toast t = Toast.makeText(this, message, Toast.LENGTH_LONG); 1340 t.setText(message); 1341 t.setGravity(Gravity.CENTER_HORIZONTAL, 0, 1342 getResources().getDimensionPixelSize(R.dimen.attachment_toast_yoffset)); 1343 t.show(); 1344 } 1345 1346 private void initAttachmentsFromIntent(Intent intent) { 1347 Bundle extras = intent.getExtras(); 1348 if (extras == null) { 1349 extras = Bundle.EMPTY; 1350 } 1351 final String action = intent.getAction(); 1352 if (!mAttachmentsChanged) { 1353 long totalSize = 0; 1354 if (extras.containsKey(EXTRA_ATTACHMENTS)) { 1355 String[] uris = (String[]) extras.getSerializable(EXTRA_ATTACHMENTS); 1356 for (String uriString : uris) { 1357 final Uri uri = Uri.parse(uriString); 1358 long size = 0; 1359 try { 1360 size = mAttachmentsView.addAttachment(mAccount, uri); 1361 } catch (AttachmentFailureException e) { 1362 LogUtils.e(LOG_TAG, e, "Error adding attachment"); 1363 showAttachmentTooBigToast(e.getErrorRes()); 1364 } 1365 totalSize += size; 1366 } 1367 } 1368 if (extras.containsKey(Intent.EXTRA_STREAM)) { 1369 if (Intent.ACTION_SEND_MULTIPLE.equals(action)) { 1370 ArrayList<Parcelable> uris = extras 1371 .getParcelableArrayList(Intent.EXTRA_STREAM); 1372 ArrayList<Attachment> attachments = new ArrayList<Attachment>(); 1373 for (Parcelable uri : uris) { 1374 try { 1375 attachments.add(mAttachmentsView.generateLocalAttachment((Uri) uri)); 1376 } catch (AttachmentFailureException e) { 1377 LogUtils.e(LOG_TAG, e, "Error adding attachment"); 1378 String maxSize = AttachmentUtils.convertToHumanReadableSize( 1379 getApplicationContext(), 1380 mAccount.settings.getMaxAttachmentSize()); 1381 showErrorToast(getString 1382 (R.string.generic_attachment_problem, maxSize)); 1383 } 1384 } 1385 totalSize += addAttachments(attachments); 1386 } else { 1387 final Uri uri = (Uri) extras.getParcelable(Intent.EXTRA_STREAM); 1388 long size = 0; 1389 try { 1390 size = mAttachmentsView.addAttachment(mAccount, uri); 1391 } catch (AttachmentFailureException e) { 1392 LogUtils.e(LOG_TAG, e, "Error adding attachment"); 1393 showAttachmentTooBigToast(e.getErrorRes()); 1394 } 1395 totalSize += size; 1396 } 1397 } 1398 1399 if (totalSize > 0) { 1400 mAttachmentsChanged = true; 1401 updateSaveUi(); 1402 } 1403 } 1404 } 1405 1406 1407 private void initQuotedTextFromRefMessage(Message refMessage, int action) { 1408 if (mRefMessage != null && (action == REPLY || action == REPLY_ALL || action == FORWARD)) { 1409 mQuotedTextView.setQuotedText(action, refMessage, action != FORWARD); 1410 } 1411 } 1412 1413 private void updateHideOrShowCcBcc() { 1414 // Its possible there is a menu item OR a button. 1415 boolean ccVisible = mCcBccView.isCcVisible(); 1416 boolean bccVisible = mCcBccView.isBccVisible(); 1417 if (mCcBccButton != null) { 1418 if (!ccVisible || !bccVisible) { 1419 mCcBccButton.setVisibility(View.VISIBLE); 1420 mCcBccButton.setText(getString(!ccVisible ? R.string.add_cc_label 1421 : R.string.add_bcc_label)); 1422 } else { 1423 mCcBccButton.setVisibility(View.INVISIBLE); 1424 } 1425 } 1426 } 1427 1428 private void showCcBcc(Bundle state) { 1429 if (state != null && state.containsKey(EXTRA_SHOW_CC)) { 1430 boolean showCc = state.getBoolean(EXTRA_SHOW_CC); 1431 boolean showBcc = state.getBoolean(EXTRA_SHOW_BCC); 1432 if (showCc || showBcc) { 1433 mCcBccView.show(false, showCc, showBcc); 1434 } 1435 } 1436 } 1437 1438 /** 1439 * Add attachment and update the compose area appropriately. 1440 * @param data 1441 */ 1442 public void addAttachmentAndUpdateView(Intent data) { 1443 addAttachmentAndUpdateView(data != null ? data.getData() : (Uri) null); 1444 } 1445 1446 public void addAttachmentAndUpdateView(Uri contentUri) { 1447 if (contentUri == null) { 1448 return; 1449 } 1450 try { 1451 addAttachmentAndUpdateView(mAttachmentsView.generateLocalAttachment(contentUri)); 1452 } catch (AttachmentFailureException e) { 1453 LogUtils.e(LOG_TAG, e, "Error adding attachment"); 1454 showErrorToast(getResources().getString( 1455 e.getErrorRes(), 1456 AttachmentUtils.convertToHumanReadableSize( 1457 getApplicationContext(), mAccount.settings.getMaxAttachmentSize()))); 1458 } 1459 } 1460 1461 public void addAttachmentAndUpdateView(Attachment attachment) { 1462 try { 1463 long size = mAttachmentsView.addAttachment(mAccount, attachment); 1464 if (size > 0) { 1465 mAttachmentsChanged = true; 1466 updateSaveUi(); 1467 } 1468 } catch (AttachmentFailureException e) { 1469 LogUtils.e(LOG_TAG, e, "Error adding attachment"); 1470 showAttachmentTooBigToast(e.getErrorRes()); 1471 } 1472 } 1473 1474 void initRecipientsFromRefMessage(Message refMessage, int action) { 1475 // Don't populate the address if this is a forward. 1476 if (action == ComposeActivity.FORWARD) { 1477 return; 1478 } 1479 initReplyRecipients(mAccount.name, refMessage, action); 1480 } 1481 1482 @VisibleForTesting 1483 void initReplyRecipients(String account, Message refMessage, int action) { 1484 // This is the email address of the current user, i.e. the one composing 1485 // the reply. 1486 final String accountEmail = Address.getEmailAddress(account).getAddress(); 1487 String[] sentToAddresses = refMessage.getToAddresses(); 1488 final Collection<String> toAddresses; 1489 String replytoAddress = refMessage.getReplyTo(); 1490 // If there is no reply to address, the reply to address is the sender. 1491 if (TextUtils.isEmpty(replytoAddress)) { 1492 replytoAddress = refMessage.getFrom(); 1493 } 1494 1495 // If this is a reply, the Cc list is empty. If this is a reply-all, the 1496 // Cc list is the union of the To and Cc recipients of the original 1497 // message, excluding the current user's email address and any addresses 1498 // already on the To list. 1499 if (action == ComposeActivity.REPLY) { 1500 toAddresses = initToRecipients(accountEmail, refMessage.getFrom(), 1501 replytoAddress, sentToAddresses); 1502 addToAddresses(toAddresses); 1503 } else if (action == ComposeActivity.REPLY_ALL) { 1504 final Set<String> ccAddresses = Sets.newHashSet(); 1505 toAddresses = initToRecipients(accountEmail, refMessage.getFrom(), 1506 replytoAddress, sentToAddresses); 1507 addToAddresses(toAddresses); 1508 addRecipients(accountEmail, ccAddresses, sentToAddresses); 1509 addRecipients(accountEmail, ccAddresses, refMessage.getCcAddresses()); 1510 addCcAddresses(ccAddresses, toAddresses); 1511 } 1512 } 1513 1514 private void addToAddresses(Collection<String> addresses) { 1515 addAddressesToList(addresses, mTo); 1516 } 1517 1518 private void addCcAddresses(Collection<String> addresses, Collection<String> toAddresses) { 1519 addCcAddressesToList(tokenizeAddressList(addresses), 1520 toAddresses != null ? tokenizeAddressList(toAddresses) : null, mCc); 1521 } 1522 1523 private void addBccAddresses(Collection<String> addresses) { 1524 addAddressesToList(addresses, mBcc); 1525 } 1526 1527 @VisibleForTesting 1528 protected void addCcAddressesToList(List<Rfc822Token[]> addresses, 1529 List<Rfc822Token[]> compareToList, RecipientEditTextView list) { 1530 String address; 1531 1532 if (compareToList == null) { 1533 for (Rfc822Token[] tokens : addresses) { 1534 for (int i = 0; i < tokens.length; i++) { 1535 address = tokens[i].toString(); 1536 list.append(address + END_TOKEN); 1537 } 1538 } 1539 } else { 1540 HashSet<String> compareTo = convertToHashSet(compareToList); 1541 for (Rfc822Token[] tokens : addresses) { 1542 for (int i = 0; i < tokens.length; i++) { 1543 address = tokens[i].toString(); 1544 // Check if this is a duplicate: 1545 if (!compareTo.contains(tokens[i].getAddress())) { 1546 // Get the address here 1547 list.append(address + END_TOKEN); 1548 } 1549 } 1550 } 1551 } 1552 } 1553 1554 private HashSet<String> convertToHashSet(List<Rfc822Token[]> list) { 1555 HashSet<String> hash = new HashSet<String>(); 1556 for (Rfc822Token[] tokens : list) { 1557 for (int i = 0; i < tokens.length; i++) { 1558 hash.add(tokens[i].getAddress()); 1559 } 1560 } 1561 return hash; 1562 } 1563 1564 protected List<Rfc822Token[]> tokenizeAddressList(Collection<String> addresses) { 1565 @VisibleForTesting 1566 List<Rfc822Token[]> tokenized = new ArrayList<Rfc822Token[]>(); 1567 1568 for (String address: addresses) { 1569 tokenized.add(Rfc822Tokenizer.tokenize(address)); 1570 } 1571 return tokenized; 1572 } 1573 1574 @VisibleForTesting 1575 void addAddressesToList(Collection<String> addresses, RecipientEditTextView list) { 1576 for (String address : addresses) { 1577 addAddressToList(address, list); 1578 } 1579 } 1580 1581 private void addAddressToList(String address, RecipientEditTextView list) { 1582 if (address == null || list == null) 1583 return; 1584 1585 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address); 1586 1587 for (int i = 0; i < tokens.length; i++) { 1588 list.append(tokens[i] + END_TOKEN); 1589 } 1590 } 1591 1592 @VisibleForTesting 1593 protected Collection<String> initToRecipients(String accountEmail, String fullSenderAddress, 1594 String replyToAddress, String[] inToAddresses) { 1595 // The To recipient is the reply-to address specified in the original 1596 // message, unless it is: 1597 // the current user OR a custom from of the current user, in which case 1598 // it's the To recipient list of the original message. 1599 // OR missing, in which case use the sender of the original message 1600 Set<String> toAddresses = Sets.newHashSet(); 1601 if (!TextUtils.isEmpty(replyToAddress) && !recipientMatchesThisAccount(replyToAddress)) { 1602 toAddresses.add(replyToAddress); 1603 } else { 1604 // In this case, the user is replying to a message in which their 1605 // current account or one of their custom from addresses is the only 1606 // recipient and they sent the original message. 1607 if (inToAddresses.length == 1 && recipientMatchesThisAccount(fullSenderAddress) 1608 && recipientMatchesThisAccount(inToAddresses[0])) { 1609 toAddresses.add(inToAddresses[0]); 1610 return toAddresses; 1611 } 1612 // This happens if the user replies to a message they originally 1613 // wrote. In this case, "reply" really means "re-send," so we 1614 // target the original recipients. This works as expected even 1615 // if the user sent the original message to themselves. 1616 for (String address : inToAddresses) { 1617 if (!recipientMatchesThisAccount(address)) { 1618 toAddresses.add(address); 1619 } 1620 } 1621 } 1622 return toAddresses; 1623 } 1624 1625 private void addRecipients(String accountAddress, Set<String> recipients, String[] addresses) { 1626 for (String email : addresses) { 1627 // Do not add this account, or any of its custom from addresses, to 1628 // the list of recipients. 1629 final String recipientAddress = Address.getEmailAddress(email).getAddress(); 1630 if (!recipientMatchesThisAccount(recipientAddress)) { 1631 recipients.add(email.replace("\"\"", "")); 1632 } 1633 } 1634 } 1635 1636 /** 1637 * A recipient matches this account if it has the same address as the 1638 * currently selected account OR one of the custom from addresses associated 1639 * with the currently selected account. 1640 * @param accountAddress currently selected account 1641 * @param recipientAddress address we are comparing with the currently selected account 1642 * @return 1643 */ 1644 protected boolean recipientMatchesThisAccount(String recipientAddress) { 1645 return ReplyFromAccount.matchesAccountOrCustomFrom(mAccount, recipientAddress, 1646 mAccount.getReplyFroms()); 1647 } 1648 1649 private void setSubject(Message refMessage, int action) { 1650 String subject = refMessage.subject; 1651 String prefix; 1652 String correctedSubject = null; 1653 if (action == ComposeActivity.COMPOSE) { 1654 prefix = ""; 1655 } else if (action == ComposeActivity.FORWARD) { 1656 prefix = getString(R.string.forward_subject_label); 1657 } else { 1658 prefix = getString(R.string.reply_subject_label); 1659 } 1660 1661 // Don't duplicate the prefix 1662 if (!TextUtils.isEmpty(subject) 1663 && subject.toLowerCase().startsWith(prefix.toLowerCase())) { 1664 correctedSubject = subject; 1665 } else { 1666 correctedSubject = String 1667 .format(getString(R.string.formatted_subject), prefix, subject); 1668 } 1669 mSubject.setText(correctedSubject); 1670 } 1671 1672 private void initRecipients() { 1673 setupRecipients(mTo); 1674 setupRecipients(mCc); 1675 setupRecipients(mBcc); 1676 } 1677 1678 private void setupRecipients(RecipientEditTextView view) { 1679 view.setAdapter(new RecipientAdapter(this, mAccount)); 1680 view.setTokenizer(new Rfc822Tokenizer()); 1681 if (mValidator == null) { 1682 final String accountName = mAccount.name; 1683 int offset = accountName.indexOf("@") + 1; 1684 String account = accountName; 1685 if (offset > -1) { 1686 account = account.substring(accountName.indexOf("@") + 1); 1687 } 1688 mValidator = new Rfc822Validator(account); 1689 } 1690 view.setValidator(mValidator); 1691 } 1692 1693 @Override 1694 public void onClick(View v) { 1695 int id = v.getId(); 1696 switch (id) { 1697 case R.id.add_cc_bcc: 1698 // Verify that cc/ bcc aren't showing. 1699 // Animate in cc/bcc. 1700 showCcBccViews(); 1701 break; 1702 case R.id.add_photo_attachment: 1703 doAttach(MIME_TYPE_PHOTO); 1704 break; 1705 case R.id.add_video_attachment: 1706 doAttach(MIME_TYPE_VIDEO); 1707 break; 1708 } 1709 } 1710 1711 @Override 1712 public boolean onCreateOptionsMenu(Menu menu) { 1713 super.onCreateOptionsMenu(menu); 1714 // Don't render any menu items when there are no accounts. 1715 if (mAccounts == null || mAccounts.length == 0) { 1716 return true; 1717 } 1718 MenuInflater inflater = getMenuInflater(); 1719 inflater.inflate(R.menu.compose_menu, menu); 1720 1721 /* 1722 * Start save in the correct enabled state. 1723 * 1) If a user launches compose from within gmail, save is disabled 1724 * until they add something, at which point, save is enabled, auto save 1725 * on exit; if the user empties everything, save is disabled, exiting does not 1726 * auto-save 1727 * 2) if a user replies/ reply all/ forwards from within gmail, save is 1728 * disabled until they change something, at which point, save is 1729 * enabled, auto save on exit; if the user empties everything, save is 1730 * disabled, exiting does not auto-save. 1731 * 3) If a user launches compose from another application and something 1732 * gets populated (attachments, recipients, body, subject, etc), save is 1733 * enabled, auto save on exit; if the user empties everything, save is 1734 * disabled, exiting does not auto-save 1735 */ 1736 mSave = menu.findItem(R.id.save); 1737 String action = getIntent() != null ? getIntent().getAction() : null; 1738 enableSave(mSavedInstanceState != null ? 1739 mSavedInstanceState.getBoolean(EXTRA_SAVE_ENABLED) 1740 : (Intent.ACTION_SEND.equals(action) 1741 || Intent.ACTION_SEND_MULTIPLE.equals(action) 1742 || Intent.ACTION_SENDTO.equals(action) 1743 || shouldSave())); 1744 1745 mSend = menu.findItem(R.id.send); 1746 MenuItem helpItem = menu.findItem(R.id.help_info_menu_item); 1747 MenuItem sendFeedbackItem = menu.findItem(R.id.feedback_menu_item); 1748 if (helpItem != null) { 1749 helpItem.setVisible(mAccount != null 1750 && mAccount.supportsCapability(AccountCapabilities.HELP_CONTENT)); 1751 } 1752 if (sendFeedbackItem != null) { 1753 sendFeedbackItem.setVisible(mAccount != null 1754 && mAccount.supportsCapability(AccountCapabilities.SEND_FEEDBACK)); 1755 } 1756 return true; 1757 } 1758 1759 @Override 1760 public boolean onPrepareOptionsMenu(Menu menu) { 1761 MenuItem ccBcc = menu.findItem(R.id.add_cc_bcc); 1762 if (ccBcc != null && mCc != null) { 1763 // Its possible there is a menu item OR a button. 1764 boolean ccFieldVisible = mCc.isShown(); 1765 boolean bccFieldVisible = mBcc.isShown(); 1766 if (!ccFieldVisible || !bccFieldVisible) { 1767 ccBcc.setVisible(true); 1768 ccBcc.setTitle(getString(!ccFieldVisible ? R.string.add_cc_label 1769 : R.string.add_bcc_label)); 1770 } else { 1771 ccBcc.setVisible(false); 1772 } 1773 } 1774 return true; 1775 } 1776 1777 @Override 1778 public boolean onOptionsItemSelected(MenuItem item) { 1779 int id = item.getItemId(); 1780 boolean handled = true; 1781 switch (id) { 1782 case R.id.add_photo_attachment: 1783 doAttach(MIME_TYPE_PHOTO); 1784 break; 1785 case R.id.add_video_attachment: 1786 doAttach(MIME_TYPE_VIDEO); 1787 break; 1788 case R.id.add_cc_bcc: 1789 showCcBccViews(); 1790 break; 1791 case R.id.save: 1792 doSave(true); 1793 break; 1794 case R.id.send: 1795 doSend(); 1796 break; 1797 case R.id.discard: 1798 doDiscard(); 1799 break; 1800 case R.id.settings: 1801 Utils.showSettings(this, mAccount); 1802 break; 1803 case android.R.id.home: 1804 onAppUpPressed(); 1805 break; 1806 case R.id.help_info_menu_item: 1807 Utils.showHelp(this, mAccount, getString(R.string.compose_help_context)); 1808 break; 1809 case R.id.feedback_menu_item: 1810 Utils.sendFeedback(this, mAccount, false); 1811 break; 1812 default: 1813 handled = false; 1814 break; 1815 } 1816 return !handled ? super.onOptionsItemSelected(item) : handled; 1817 } 1818 1819 @Override 1820 public void onBackPressed() { 1821 // If we are showing the wait fragment, just exit. 1822 if (getWaitFragment() != null) { 1823 finish(); 1824 } else { 1825 super.onBackPressed(); 1826 } 1827 } 1828 1829 /** 1830 * Carries out the "up" action in the action bar. 1831 */ 1832 private void onAppUpPressed() { 1833 if (mLaunchedFromEmail) { 1834 // If this was started from Gmail, simply treat app up as the system back button, so 1835 // that the last view is restored. 1836 onBackPressed(); 1837 return; 1838 } 1839 1840 // Fire the main activity to ensure it launches the "top" screen of mail. 1841 // Since the main Activity is singleTask, it should revive that task if it was already 1842 // started. 1843 final Intent mailIntent = Utils.createViewInboxIntent(mAccount); 1844 mailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | 1845 Intent.FLAG_ACTIVITY_TASK_ON_HOME); 1846 startActivity(mailIntent); 1847 finish(); 1848 } 1849 1850 private void doSend() { 1851 sendOrSaveWithSanityChecks(false, true, false, false); 1852 } 1853 1854 private void doSave(boolean showToast) { 1855 sendOrSaveWithSanityChecks(true, showToast, false, false); 1856 } 1857 1858 @VisibleForTesting 1859 public interface SendOrSaveCallback { 1860 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask); 1861 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage, Message message); 1862 public Message getMessage(); 1863 public void sendOrSaveFinished(SendOrSaveTask sendOrSaveTask, boolean success); 1864 } 1865 1866 @VisibleForTesting 1867 public static class SendOrSaveTask implements Runnable { 1868 private final Context mContext; 1869 @VisibleForTesting 1870 public final SendOrSaveCallback mSendOrSaveCallback; 1871 @VisibleForTesting 1872 public final SendOrSaveMessage mSendOrSaveMessage; 1873 private ReplyFromAccount mExistingDraftAccount; 1874 1875 public SendOrSaveTask(Context context, SendOrSaveMessage message, 1876 SendOrSaveCallback callback, ReplyFromAccount draftAccount) { 1877 mContext = context; 1878 mSendOrSaveCallback = callback; 1879 mSendOrSaveMessage = message; 1880 mExistingDraftAccount = draftAccount; 1881 } 1882 1883 @Override 1884 public void run() { 1885 final SendOrSaveMessage sendOrSaveMessage = mSendOrSaveMessage; 1886 1887 final ReplyFromAccount selectedAccount = sendOrSaveMessage.mAccount; 1888 Message message = mSendOrSaveCallback.getMessage(); 1889 long messageId = message != null ? message.id : UIProvider.INVALID_MESSAGE_ID; 1890 // If a previous draft has been saved, in an account that is different 1891 // than what the user wants to send from, remove the old draft, and treat this 1892 // as a new message 1893 if (mExistingDraftAccount != null 1894 && !selectedAccount.account.uri.equals(mExistingDraftAccount.account.uri)) { 1895 if (messageId != UIProvider.INVALID_MESSAGE_ID) { 1896 ContentResolver resolver = mContext.getContentResolver(); 1897 ContentValues values = new ContentValues(); 1898 values.put(BaseColumns._ID, messageId); 1899 if (mExistingDraftAccount.account.expungeMessageUri != null) { 1900 new ContentProviderTask.UpdateTask() 1901 .run(resolver, mExistingDraftAccount.account.expungeMessageUri, 1902 values, null, null); 1903 } else { 1904 // TODO(mindyp) delete the conversation. 1905 } 1906 // reset messageId to 0, so a new message will be created 1907 messageId = UIProvider.INVALID_MESSAGE_ID; 1908 } 1909 } 1910 1911 final long messageIdToSave = messageId; 1912 sendOrSaveMessage(messageIdToSave, sendOrSaveMessage, selectedAccount, message); 1913 1914 if (!sendOrSaveMessage.mSave) { 1915 UIProvider.incrementRecipientsTimesContacted(mContext, 1916 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO)); 1917 UIProvider.incrementRecipientsTimesContacted(mContext, 1918 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC)); 1919 UIProvider.incrementRecipientsTimesContacted(mContext, 1920 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC)); 1921 } 1922 mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true); 1923 } 1924 1925 /** 1926 * Send or Save a message. 1927 */ 1928 private void sendOrSaveMessage(long messageIdToSave, SendOrSaveMessage sendOrSaveMessage, 1929 ReplyFromAccount selectedAccount, Message message) { 1930 final ContentResolver resolver = mContext.getContentResolver(); 1931 final boolean updateExistingMessage = messageIdToSave != UIProvider.INVALID_MESSAGE_ID; 1932 1933 final String accountMethod = sendOrSaveMessage.mSave ? 1934 UIProvider.AccountCallMethods.SAVE_MESSAGE : 1935 UIProvider.AccountCallMethods.SEND_MESSAGE; 1936 1937 try { 1938 if (updateExistingMessage) { 1939 sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave); 1940 1941 final Bundle result = callAccountSendSaveMethod(resolver, 1942 selectedAccount.account, accountMethod, sendOrSaveMessage); 1943 if (result == null) { 1944 // TODO(pwestbro): Once Email supports the call api, remove this block 1945 // If null was returned, then the provider didn't handle the call method 1946 final Uri updateUri = Uri.parse(sendOrSaveMessage.mSave ? 1947 message.saveUri : message.sendUri); 1948 resolver.update(updateUri, sendOrSaveMessage.mValues, null, null); 1949 } 1950 } else { 1951 final Uri messageUri; 1952 final Bundle result = callAccountSendSaveMethod(resolver, 1953 selectedAccount.account, accountMethod, sendOrSaveMessage); 1954 if (result != null) { 1955 // If a non-null value was returned, then the provider handled the call 1956 // method 1957 messageUri = result.getParcelable(UIProvider.MessageColumns.URI); 1958 } else { 1959 // TODO(pwestbro): Once Email supports the call api, remove this block 1960 messageUri = resolver.insert( 1961 sendOrSaveMessage.mSave ? selectedAccount.account.saveDraftUri 1962 : selectedAccount.account.sendMessageUri, 1963 sendOrSaveMessage.mValues); 1964 } 1965 if (sendOrSaveMessage.mSave && messageUri != null) { 1966 final Cursor messageCursor = resolver.query(messageUri, 1967 UIProvider.MESSAGE_PROJECTION, null, null, null); 1968 if (messageCursor != null) { 1969 try { 1970 if (messageCursor.moveToFirst()) { 1971 // Broadcast notification that a new message has 1972 // been allocated 1973 mSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage, 1974 new Message(messageCursor)); 1975 } 1976 } finally { 1977 messageCursor.close(); 1978 } 1979 } 1980 } 1981 } 1982 } finally { 1983 // Close any opened file descriptors 1984 closeOpenedAttachmentFds(sendOrSaveMessage); 1985 } 1986 } 1987 1988 private void closeOpenedAttachmentFds(SendOrSaveMessage sendOrSaveMessage) { 1989 final Bundle openedFds = sendOrSaveMessage.attachmentFds(); 1990 if (openedFds != null) { 1991 final Set<String> keys = openedFds.keySet(); 1992 for (String key : keys) { 1993 final ParcelFileDescriptor fd = openedFds.getParcelable(key); 1994 if (fd != null) { 1995 try { 1996 fd.close(); 1997 } catch (IOException e) { 1998 // Do nothing 1999 } 2000 } 2001 } 2002 } 2003 } 2004 2005 /** 2006 * Use the {@link ContentResolver#call()} method to send or save the message. 2007 * 2008 * If this was successful, this method will return an non-null Bundle instance 2009 */ 2010 private Bundle callAccountSendSaveMethod(ContentResolver resolver, Account account, 2011 String method, SendOrSaveMessage sendOrSaveMessage) { 2012 // Copy all of the values from the content values to the bundle 2013 final Bundle methodExtras = new Bundle(sendOrSaveMessage.mValues.size()); 2014 final Set<Entry<String, Object>> valueSet = sendOrSaveMessage.mValues.valueSet(); 2015 2016 for (Entry<String, Object> entry : valueSet) { 2017 final Object entryValue = entry.getValue(); 2018 final String key = entry.getKey(); 2019 if (entryValue instanceof String) { 2020 methodExtras.putString(key, (String)entryValue); 2021 } else if (entryValue instanceof Boolean) { 2022 methodExtras.putBoolean(key, (Boolean)entryValue); 2023 } else if (entryValue instanceof Integer) { 2024 methodExtras.putInt(key, (Integer)entryValue); 2025 } else if (entryValue instanceof Long) { 2026 methodExtras.putLong(key, (Long)entryValue); 2027 } else { 2028 LogUtils.wtf(LOG_TAG, "Unexpected object type: %s", 2029 entryValue.getClass().getName()); 2030 } 2031 } 2032 2033 // If the SendOrSaveMessage has some opened fds, add them to the bundle 2034 final Bundle fdMap = sendOrSaveMessage.attachmentFds(); 2035 if (fdMap != null) { 2036 methodExtras.putParcelable( 2037 UIProvider.SendOrSaveMethodParamKeys.OPENED_FD_MAP, fdMap); 2038 } 2039 2040 return resolver.call(account.uri, method, account.uri.toString(), methodExtras); 2041 } 2042 } 2043 2044 @VisibleForTesting 2045 public static class SendOrSaveMessage { 2046 final ReplyFromAccount mAccount; 2047 final ContentValues mValues; 2048 final String mRefMessageId; 2049 @VisibleForTesting 2050 public final boolean mSave; 2051 final int mRequestId; 2052 private final Bundle mAttachmentFds; 2053 2054 public SendOrSaveMessage(Context context, ReplyFromAccount account, ContentValues values, 2055 String refMessageId, List<Attachment> attachments, boolean save) { 2056 mAccount = account; 2057 mValues = values; 2058 mRefMessageId = refMessageId; 2059 mSave = save; 2060 mRequestId = mValues.hashCode() ^ hashCode(); 2061 2062 mAttachmentFds = initializeAttachmentFds(context, attachments); 2063 } 2064 2065 int requestId() { 2066 return mRequestId; 2067 } 2068 2069 Bundle attachmentFds() { 2070 return mAttachmentFds; 2071 } 2072 2073 /** 2074 * Opens {@link ParcelFileDescriptor} for each of the attachments. This method must be 2075 * called before the ComposeActivity finishes. 2076 * Note: The caller is responsible for closing these file descriptors. 2077 */ 2078 private Bundle initializeAttachmentFds(Context context, List<Attachment> attachments) { 2079 if (attachments == null || attachments.size() == 0) { 2080 return null; 2081 } 2082 2083 final Bundle result = new Bundle(attachments.size()); 2084 final ContentResolver resolver = context.getContentResolver(); 2085 2086 for (Attachment attachment : attachments) { 2087 if (attachment == null || Utils.isEmpty(attachment.contentUri)) { 2088 continue; 2089 } 2090 2091 ParcelFileDescriptor fileDescriptor; 2092 try { 2093 fileDescriptor = resolver.openFileDescriptor(attachment.contentUri, "r"); 2094 } catch (FileNotFoundException e) { 2095 LogUtils.e(LOG_TAG, e, "Exception attempting to open attachment"); 2096 fileDescriptor = null; 2097 } 2098 2099 if (fileDescriptor != null) { 2100 result.putParcelable(attachment.contentUri.toString(), fileDescriptor); 2101 } 2102 } 2103 2104 return result; 2105 } 2106 } 2107 2108 /** 2109 * Get the to recipients. 2110 */ 2111 public String[] getToAddresses() { 2112 return getAddressesFromList(mTo); 2113 } 2114 2115 /** 2116 * Get the cc recipients. 2117 */ 2118 public String[] getCcAddresses() { 2119 return getAddressesFromList(mCc); 2120 } 2121 2122 /** 2123 * Get the bcc recipients. 2124 */ 2125 public String[] getBccAddresses() { 2126 return getAddressesFromList(mBcc); 2127 } 2128 2129 public String[] getAddressesFromList(RecipientEditTextView list) { 2130 if (list == null) { 2131 return new String[0]; 2132 } 2133 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(list.getText()); 2134 int count = tokens.length; 2135 String[] result = new String[count]; 2136 for (int i = 0; i < count; i++) { 2137 result[i] = tokens[i].toString(); 2138 } 2139 return result; 2140 } 2141 2142 /** 2143 * Check for invalid email addresses. 2144 * @param to String array of email addresses to check. 2145 * @param wrongEmailsOut Emails addresses that were invalid. 2146 */ 2147 public void checkInvalidEmails(String[] to, List<String> wrongEmailsOut) { 2148 if (mValidator == null) { 2149 return; 2150 } 2151 for (String email : to) { 2152 if (!mValidator.isValid(email)) { 2153 wrongEmailsOut.add(email); 2154 } 2155 } 2156 } 2157 2158 /** 2159 * Show an error because the user has entered an invalid recipient. 2160 * @param message 2161 */ 2162 public void showRecipientErrorDialog(String message) { 2163 // Only 1 invalid recipients error dialog should be allowed up at a 2164 // time. 2165 if (mRecipientErrorDialog != null) { 2166 mRecipientErrorDialog.dismiss(); 2167 } 2168 mRecipientErrorDialog = new AlertDialog.Builder(this).setMessage(message).setTitle( 2169 R.string.recipient_error_dialog_title) 2170 .setIconAttribute(android.R.attr.alertDialogIcon) 2171 .setPositiveButton( 2172 R.string.ok, new Dialog.OnClickListener() { 2173 @Override 2174 public void onClick(DialogInterface dialog, int which) { 2175 // after the user dismisses the recipient error 2176 // dialog we want to make sure to refocus the 2177 // recipient to field so they can fix the issue 2178 // easily 2179 if (mTo != null) { 2180 mTo.requestFocus(); 2181 } 2182 mRecipientErrorDialog = null; 2183 } 2184 }).show(); 2185 } 2186 2187 /** 2188 * Update the state of the UI based on whether or not the current draft 2189 * needs to be saved and the message is not empty. 2190 */ 2191 public void updateSaveUi() { 2192 if (mSave != null) { 2193 mSave.setEnabled((shouldSave() && !isBlank())); 2194 } 2195 } 2196 2197 /** 2198 * Returns true if we need to save the current draft. 2199 */ 2200 private boolean shouldSave() { 2201 synchronized (mDraftLock) { 2202 // The message should only be saved if: 2203 // It hasn't been sent AND 2204 // Some text has been added to the message OR 2205 // an attachment has been added or removed 2206 // AND there is actually something in the draft to save. 2207 return (mTextChanged || mAttachmentsChanged || mReplyFromChanged) 2208 && !isBlank(); 2209 } 2210 } 2211 2212 /** 2213 * Check if all fields are blank. 2214 * @return boolean 2215 */ 2216 public boolean isBlank() { 2217 return mSubject.getText().length() == 0 2218 && (mBodyView.getText().length() == 0 || getSignatureStartPosition(mSignature, 2219 mBodyView.getText().toString()) == 0) 2220 && mTo.length() == 0 2221 && mCc.length() == 0 && mBcc.length() == 0 2222 && mAttachmentsView.getAttachments().size() == 0; 2223 } 2224 2225 @VisibleForTesting 2226 protected int getSignatureStartPosition(String signature, String bodyText) { 2227 int startPos = -1; 2228 2229 if (TextUtils.isEmpty(signature) || TextUtils.isEmpty(bodyText)) { 2230 return startPos; 2231 } 2232 2233 int bodyLength = bodyText.length(); 2234 int signatureLength = signature.length(); 2235 String printableVersion = convertToPrintableSignature(signature); 2236 int printableLength = printableVersion.length(); 2237 2238 if (bodyLength >= printableLength 2239 && bodyText.substring(bodyLength - printableLength) 2240 .equals(printableVersion)) { 2241 startPos = bodyLength - printableLength; 2242 } else if (bodyLength >= signatureLength 2243 && bodyText.substring(bodyLength - signatureLength) 2244 .equals(signature)) { 2245 startPos = bodyLength - signatureLength; 2246 } 2247 return startPos; 2248 } 2249 2250 /** 2251 * Allows any changes made by the user to be ignored. Called when the user 2252 * decides to discard a draft. 2253 */ 2254 private void discardChanges() { 2255 mTextChanged = false; 2256 mAttachmentsChanged = false; 2257 mReplyFromChanged = false; 2258 } 2259 2260 /** 2261 * @param body 2262 * @param save 2263 * @param showToast 2264 * @return Whether the send or save succeeded. 2265 */ 2266 protected boolean sendOrSaveWithSanityChecks(final boolean save, final boolean showToast, 2267 final boolean orientationChanged, final boolean autoSend) { 2268 if (mAccounts == null || mAccount == null) { 2269 Toast.makeText(this, R.string.send_failed, Toast.LENGTH_SHORT).show(); 2270 if (autoSend) { 2271 finish(); 2272 } 2273 return false; 2274 } 2275 2276 String[] to, cc, bcc; 2277 Editable body = mBodyView.getEditableText(); 2278 if (orientationChanged) { 2279 to = cc = bcc = new String[0]; 2280 } else { 2281 to = getToAddresses(); 2282 cc = getCcAddresses(); 2283 bcc = getBccAddresses(); 2284 } 2285 2286 // Don't let the user send to nobody (but it's okay to save a message 2287 // with no recipients) 2288 if (!save && (to.length == 0 && cc.length == 0 && bcc.length == 0)) { 2289 showRecipientErrorDialog(getString(R.string.recipient_needed)); 2290 return false; 2291 } 2292 2293 List<String> wrongEmails = new ArrayList<String>(); 2294 if (!save) { 2295 checkInvalidEmails(to, wrongEmails); 2296 checkInvalidEmails(cc, wrongEmails); 2297 checkInvalidEmails(bcc, wrongEmails); 2298 } 2299 2300 // Don't let the user send an email with invalid recipients 2301 if (wrongEmails.size() > 0) { 2302 String errorText = String.format(getString(R.string.invalid_recipient), 2303 wrongEmails.get(0)); 2304 showRecipientErrorDialog(errorText); 2305 return false; 2306 } 2307 2308 DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { 2309 @Override 2310 public void onClick(DialogInterface dialog, int which) { 2311 sendOrSave(mBodyView.getEditableText(), save, showToast, orientationChanged); 2312 } 2313 }; 2314 2315 // Show a warning before sending only if there are no attachments. 2316 if (!save) { 2317 if (mAttachmentsView.getAttachments().isEmpty() && showEmptyTextWarnings()) { 2318 boolean warnAboutEmptySubject = isSubjectEmpty(); 2319 boolean emptyBody = TextUtils.getTrimmedLength(body) == 0; 2320 2321 // A warning about an empty body may not be warranted when 2322 // forwarding mails, since a common use case is to forward 2323 // quoted text and not append any more text. 2324 boolean warnAboutEmptyBody = emptyBody && (!mForward || isBodyEmpty()); 2325 2326 // When we bring up a dialog warning the user about a send, 2327 // assume that they accept sending the message. If they do not, 2328 // the dialog listener is required to enable sending again. 2329 if (warnAboutEmptySubject) { 2330 showSendConfirmDialog(R.string.confirm_send_message_with_no_subject, listener); 2331 return true; 2332 } 2333 2334 if (warnAboutEmptyBody) { 2335 showSendConfirmDialog(R.string.confirm_send_message_with_no_body, listener); 2336 return true; 2337 } 2338 } 2339 // Ask for confirmation to send (if always required) 2340 if (showSendConfirmation()) { 2341 showSendConfirmDialog(R.string.confirm_send_message, listener); 2342 return true; 2343 } 2344 } 2345 2346 sendOrSave(body, save, showToast, false); 2347 return true; 2348 } 2349 2350 /** 2351 * Returns a boolean indicating whether warnings should be shown for empty 2352 * subject and body fields 2353 * 2354 * @return True if a warning should be shown for empty text fields 2355 */ 2356 protected boolean showEmptyTextWarnings() { 2357 return mAttachmentsView.getAttachments().size() == 0; 2358 } 2359 2360 /** 2361 * Returns a boolean indicating whether the user should confirm each send 2362 * 2363 * @return True if a warning should be on each send 2364 */ 2365 protected boolean showSendConfirmation() { 2366 return mCachedSettings != null ? mCachedSettings.confirmSend : false; 2367 } 2368 2369 private void showSendConfirmDialog(int messageId, DialogInterface.OnClickListener listener) { 2370 if (mSendConfirmDialog != null) { 2371 mSendConfirmDialog.dismiss(); 2372 mSendConfirmDialog = null; 2373 } 2374 mSendConfirmDialog = new AlertDialog.Builder(this).setMessage(messageId) 2375 .setTitle(R.string.confirm_send_title) 2376 .setIconAttribute(android.R.attr.alertDialogIcon) 2377 .setPositiveButton(R.string.send, listener) 2378 .setNegativeButton(R.string.cancel, this) 2379 .show(); 2380 } 2381 2382 /** 2383 * Returns whether the ComposeArea believes there is any text in the body of 2384 * the composition. TODO: When ComposeArea controls the Body as well, add 2385 * that here. 2386 */ 2387 public boolean isBodyEmpty() { 2388 return !mQuotedTextView.isTextIncluded(); 2389 } 2390 2391 /** 2392 * Test to see if the subject is empty. 2393 * 2394 * @return boolean. 2395 */ 2396 // TODO: this will likely go away when composeArea.focus() is implemented 2397 // after all the widget control is moved over. 2398 public boolean isSubjectEmpty() { 2399 return TextUtils.getTrimmedLength(mSubject.getText()) == 0; 2400 } 2401 2402 /* package */ 2403 static int sendOrSaveInternal(Context context, ReplyFromAccount replyFromAccount, 2404 Message message, final Message refMessage, Spanned body, final CharSequence quotedText, 2405 SendOrSaveCallback callback, Handler handler, boolean save, int composeMode, 2406 ReplyFromAccount draftAccount) { 2407 ContentValues values = new ContentValues(); 2408 2409 String refMessageId = refMessage != null ? refMessage.uri.toString() : ""; 2410 2411 MessageModification.putToAddresses(values, message.getToAddresses()); 2412 MessageModification.putCcAddresses(values, message.getCcAddresses()); 2413 MessageModification.putBccAddresses(values, message.getBccAddresses()); 2414 2415 MessageModification.putCustomFromAddress(values, message.getFrom()); 2416 2417 MessageModification.putSubject(values, message.subject); 2418 String htmlBody = Html.toHtml(new SpannableString(body.toString())); 2419 2420 boolean includeQuotedText = !TextUtils.isEmpty(quotedText); 2421 StringBuilder fullBody = new StringBuilder(htmlBody); 2422 if (includeQuotedText) { 2423 // HTML gets converted to text for now 2424 final String text = quotedText.toString(); 2425 if (QuotedTextView.containsQuotedText(text)) { 2426 int pos = QuotedTextView.getQuotedTextOffset(text); 2427 final int quoteStartPos = fullBody.length() + pos; 2428 fullBody.append(text); 2429 MessageModification.putQuoteStartPos(values, quoteStartPos); 2430 MessageModification.putForward(values, composeMode == ComposeActivity.FORWARD); 2431 MessageModification.putAppendRefMessageContent(values, includeQuotedText); 2432 } else { 2433 LogUtils.w(LOG_TAG, "Couldn't find quoted text"); 2434 // This shouldn't happen, but just use what we have, 2435 // and don't do server-side expansion 2436 fullBody.append(text); 2437 } 2438 } 2439 int draftType = getDraftType(composeMode); 2440 MessageModification.putDraftType(values, draftType); 2441 if (refMessage != null) { 2442 if (!TextUtils.isEmpty(refMessage.bodyHtml)) { 2443 MessageModification.putBodyHtml(values, fullBody.toString()); 2444 } 2445 if (!TextUtils.isEmpty(refMessage.bodyText)) { 2446 MessageModification.putBody(values, 2447 Utils.convertHtmlToPlainText(fullBody.toString()).toString()); 2448 } 2449 } else { 2450 MessageModification.putBodyHtml(values, fullBody.toString()); 2451 MessageModification.putBody(values, Utils.convertHtmlToPlainText(fullBody.toString()) 2452 .toString()); 2453 } 2454 MessageModification.putAttachments(values, message.getAttachments()); 2455 if (!TextUtils.isEmpty(refMessageId)) { 2456 MessageModification.putRefMessageId(values, refMessageId); 2457 } 2458 SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(context, replyFromAccount, 2459 values, refMessageId, message.getAttachments(), save); 2460 SendOrSaveTask sendOrSaveTask = new SendOrSaveTask(context, sendOrSaveMessage, callback, 2461 draftAccount); 2462 2463 callback.initializeSendOrSave(sendOrSaveTask); 2464 // Do the send/save action on the specified handler to avoid possible 2465 // ANRs 2466 handler.post(sendOrSaveTask); 2467 2468 return sendOrSaveMessage.requestId(); 2469 } 2470 2471 private static int getDraftType(int mode) { 2472 int draftType = -1; 2473 switch (mode) { 2474 case ComposeActivity.COMPOSE: 2475 draftType = DraftType.COMPOSE; 2476 break; 2477 case ComposeActivity.REPLY: 2478 draftType = DraftType.REPLY; 2479 break; 2480 case ComposeActivity.REPLY_ALL: 2481 draftType = DraftType.REPLY_ALL; 2482 break; 2483 case ComposeActivity.FORWARD: 2484 draftType = DraftType.FORWARD; 2485 break; 2486 } 2487 return draftType; 2488 } 2489 2490 private void sendOrSave(Spanned body, boolean save, boolean showToast, 2491 boolean orientationChanged) { 2492 // Check if user is a monkey. Monkeys can compose and hit send 2493 // button but are not allowed to send anything off the device. 2494 if (ActivityManager.isUserAMonkey()) { 2495 return; 2496 } 2497 2498 String[] to, cc, bcc; 2499 if (orientationChanged) { 2500 to = cc = bcc = new String[0]; 2501 } else { 2502 to = getToAddresses(); 2503 cc = getCcAddresses(); 2504 bcc = getBccAddresses(); 2505 } 2506 2507 SendOrSaveCallback callback = new SendOrSaveCallback() { 2508 // FIXME: unused 2509 private int mRestoredRequestId; 2510 2511 @Override 2512 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask) { 2513 synchronized (mActiveTasks) { 2514 int numTasks = mActiveTasks.size(); 2515 if (numTasks == 0) { 2516 // Start service so we won't be killed if this app is 2517 // put in the background. 2518 startService(new Intent(ComposeActivity.this, EmptyService.class)); 2519 } 2520 2521 mActiveTasks.add(sendOrSaveTask); 2522 } 2523 if (sTestSendOrSaveCallback != null) { 2524 sTestSendOrSaveCallback.initializeSendOrSave(sendOrSaveTask); 2525 } 2526 } 2527 2528 @Override 2529 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage, 2530 Message message) { 2531 synchronized (mDraftLock) { 2532 mDraftAccount = sendOrSaveMessage.mAccount; 2533 mDraftId = message.id; 2534 mDraft = message; 2535 if (sRequestMessageIdMap != null) { 2536 sRequestMessageIdMap.put(sendOrSaveMessage.requestId(), mDraftId); 2537 } 2538 // Cache request message map, in case the process is killed 2539 saveRequestMap(); 2540 } 2541 if (sTestSendOrSaveCallback != null) { 2542 sTestSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage, message); 2543 } 2544 } 2545 2546 @Override 2547 public Message getMessage() { 2548 synchronized (mDraftLock) { 2549 return mDraft; 2550 } 2551 } 2552 2553 @Override 2554 public void sendOrSaveFinished(SendOrSaveTask task, boolean success) { 2555 // Update the last sent from account. 2556 if (mAccount != null) { 2557 MailAppProvider.getInstance().setLastSentFromAccount(mAccount.uri.toString()); 2558 } 2559 if (success) { 2560 // Successfully sent or saved so reset change markers 2561 discardChanges(); 2562 } else { 2563 // A failure happened with saving/sending the draft 2564 // TODO(pwestbro): add a better string that should be used 2565 // when failing to send or save 2566 Toast.makeText(ComposeActivity.this, R.string.send_failed, Toast.LENGTH_SHORT) 2567 .show(); 2568 } 2569 2570 int numTasks; 2571 synchronized (mActiveTasks) { 2572 // Remove the task from the list of active tasks 2573 mActiveTasks.remove(task); 2574 numTasks = mActiveTasks.size(); 2575 } 2576 2577 if (numTasks == 0) { 2578 // Stop service so we can be killed. 2579 stopService(new Intent(ComposeActivity.this, EmptyService.class)); 2580 } 2581 if (sTestSendOrSaveCallback != null) { 2582 sTestSendOrSaveCallback.sendOrSaveFinished(task, success); 2583 } 2584 } 2585 }; 2586 2587 // Get the selected account if the from spinner has been setup. 2588 ReplyFromAccount selectedAccount = mReplyFromAccount; 2589 String fromAddress = selectedAccount.name; 2590 if (selectedAccount == null || fromAddress == null) { 2591 // We don't have either the selected account or from address, 2592 // use mAccount. 2593 selectedAccount = mReplyFromAccount; 2594 fromAddress = mAccount.name; 2595 } 2596 2597 if (mSendSaveTaskHandler == null) { 2598 HandlerThread handlerThread = new HandlerThread("Send Message Task Thread"); 2599 handlerThread.start(); 2600 2601 mSendSaveTaskHandler = new Handler(handlerThread.getLooper()); 2602 } 2603 2604 Message msg = createMessage(mReplyFromAccount, getMode()); 2605 mRequestId = sendOrSaveInternal(this, mReplyFromAccount, msg, mRefMessage, body, 2606 mQuotedTextView.getQuotedTextIfIncluded(), callback, 2607 mSendSaveTaskHandler, save, mComposeMode, mDraftAccount); 2608 2609 if (mRecipient != null && mRecipient.equals(mAccount.name)) { 2610 mRecipient = selectedAccount.name; 2611 } 2612 setAccount(selectedAccount.account); 2613 2614 // Don't display the toast if the user is just changing the orientation, 2615 // but we still need to save the draft to the cursor because this is how we restore 2616 // the attachments when the configuration change completes. 2617 if (showToast && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) { 2618 Toast.makeText(this, save ? R.string.message_saved : R.string.sending_message, 2619 Toast.LENGTH_LONG).show(); 2620 } 2621 2622 // Need to update variables here because the send or save completes 2623 // asynchronously even though the toast shows right away. 2624 discardChanges(); 2625 updateSaveUi(); 2626 2627 // If we are sending, finish the activity 2628 if (!save) { 2629 finish(); 2630 } 2631 } 2632 2633 /** 2634 * Save the state of the request messageid map. This allows for the Gmail 2635 * process to be killed, but and still allow for ComposeActivity instances 2636 * to be recreated correctly. 2637 */ 2638 private void saveRequestMap() { 2639 // TODO: store the request map in user preferences. 2640 } 2641 2642 private void doAttach(String type) { 2643 Intent i = new Intent(Intent.ACTION_GET_CONTENT); 2644 i.addCategory(Intent.CATEGORY_OPENABLE); 2645 i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 2646 i.setType(type); 2647 mAddingAttachment = true; 2648 startActivityForResult(Intent.createChooser(i, getText(R.string.select_attachment_type)), 2649 RESULT_PICK_ATTACHMENT); 2650 } 2651 2652 private void showCcBccViews() { 2653 mCcBccView.show(true, true, true); 2654 if (mCcBccButton != null) { 2655 mCcBccButton.setVisibility(View.INVISIBLE); 2656 } 2657 } 2658 2659 @Override 2660 public boolean onNavigationItemSelected(int position, long itemId) { 2661 int initialComposeMode = mComposeMode; 2662 if (position == ComposeActivity.REPLY) { 2663 mComposeMode = ComposeActivity.REPLY; 2664 } else if (position == ComposeActivity.REPLY_ALL) { 2665 mComposeMode = ComposeActivity.REPLY_ALL; 2666 } else if (position == ComposeActivity.FORWARD) { 2667 mComposeMode = ComposeActivity.FORWARD; 2668 } 2669 clearChangeListeners(); 2670 if (initialComposeMode != mComposeMode) { 2671 resetMessageForModeChange(); 2672 if (mRefMessage != null) { 2673 setFieldsFromRefMessage(mComposeMode); 2674 } 2675 boolean showCc = false; 2676 boolean showBcc = false; 2677 if (mDraft != null) { 2678 // Following desktop behavior, if the user has added a BCC 2679 // field to a draft, we show it regardless of compose mode. 2680 showBcc = !TextUtils.isEmpty(mDraft.getBcc()); 2681 // Use the draft to determine what to populate. 2682 // If the Bcc field is showing, show the Cc field whether it is populated or not. 2683 showCc = showBcc 2684 || (!TextUtils.isEmpty(mDraft.getCc()) && mComposeMode == REPLY_ALL); 2685 } 2686 if (mRefMessage != null) { 2687 showCc = !TextUtils.isEmpty(mCc.getText()); 2688 showBcc = !TextUtils.isEmpty(mBcc.getText()); 2689 } 2690 mCcBccView.show(false, showCc, showBcc); 2691 } 2692 updateHideOrShowCcBcc(); 2693 initChangeListeners(); 2694 return true; 2695 } 2696 2697 @VisibleForTesting 2698 protected void resetMessageForModeChange() { 2699 // When switching between reply, reply all, forward, 2700 // follow the behavior of webview. 2701 // The contents of the following fields are cleared 2702 // so that they can be populated directly from the 2703 // ref message: 2704 // 1) Any recipient fields 2705 // 2) The subject 2706 mTo.setText(""); 2707 mCc.setText(""); 2708 mBcc.setText(""); 2709 // Any edits to the subject are replaced with the original subject. 2710 mSubject.setText(""); 2711 2712 // Any changes to the contents of the following fields are kept: 2713 // 1) Body 2714 // 2) Attachments 2715 // If the user made changes to attachments, keep their changes. 2716 if (!mAttachmentsChanged) { 2717 mAttachmentsView.deleteAllAttachments(); 2718 } 2719 } 2720 2721 private class ComposeModeAdapter extends ArrayAdapter<String> { 2722 2723 private LayoutInflater mInflater; 2724 2725 public ComposeModeAdapter(Context context) { 2726 super(context, R.layout.compose_mode_item, R.id.mode, getResources() 2727 .getStringArray(R.array.compose_modes)); 2728 } 2729 2730 private LayoutInflater getInflater() { 2731 if (mInflater == null) { 2732 mInflater = LayoutInflater.from(getContext()); 2733 } 2734 return mInflater; 2735 } 2736 2737 @Override 2738 public View getView(int position, View convertView, ViewGroup parent) { 2739 if (convertView == null) { 2740 convertView = getInflater().inflate(R.layout.compose_mode_display_item, null); 2741 } 2742 ((TextView) convertView.findViewById(R.id.mode)).setText(getItem(position)); 2743 return super.getView(position, convertView, parent); 2744 } 2745 } 2746 2747 @Override 2748 public void onRespondInline(String text) { 2749 appendToBody(text, false); 2750 mQuotedTextView.setUpperDividerVisible(false); 2751 mRespondedInline = true; 2752 if (!mBodyView.hasFocus()) { 2753 mBodyView.requestFocus(); 2754 } 2755 } 2756 2757 /** 2758 * Append text to the body of the message. If there is no existing body 2759 * text, just sets the body to text. 2760 * 2761 * @param text 2762 * @param withSignature True to append a signature. 2763 */ 2764 public void appendToBody(CharSequence text, boolean withSignature) { 2765 Editable bodyText = mBodyView.getEditableText(); 2766 if (bodyText != null && bodyText.length() > 0) { 2767 bodyText.append(text); 2768 } else { 2769 setBody(text, withSignature); 2770 } 2771 } 2772 2773 /** 2774 * Set the body of the message. 2775 * 2776 * @param text 2777 * @param withSignature True to append a signature. 2778 */ 2779 public void setBody(CharSequence text, boolean withSignature) { 2780 mBodyView.setText(text); 2781 if (withSignature) { 2782 appendSignature(); 2783 } 2784 } 2785 2786 private void appendSignature() { 2787 String newSignature = mCachedSettings != null ? mCachedSettings.signature : null; 2788 boolean hasFocus = mBodyView.hasFocus(); 2789 int signaturePos = getSignatureStartPosition(mSignature, mBodyView.getText().toString()); 2790 if (!TextUtils.equals(newSignature, mSignature) || signaturePos < 0) { 2791 mSignature = newSignature; 2792 if (!TextUtils.isEmpty(mSignature)) { 2793 // Appending a signature does not count as changing text. 2794 mBodyView.removeTextChangedListener(this); 2795 mBodyView.append(convertToPrintableSignature(mSignature)); 2796 mBodyView.addTextChangedListener(this); 2797 } 2798 if (hasFocus) { 2799 focusBody(); 2800 } 2801 } 2802 } 2803 2804 private String convertToPrintableSignature(String signature) { 2805 String signatureResource = getResources().getString(R.string.signature); 2806 if (signature == null) { 2807 signature = ""; 2808 } 2809 return String.format(signatureResource, signature); 2810 } 2811 2812 @Override 2813 public void onAccountChanged() { 2814 mReplyFromAccount = mFromSpinner.getCurrentAccount(); 2815 if (!mAccount.equals(mReplyFromAccount.account)) { 2816 // Clear a signature, if there was one. 2817 mBodyView.removeTextChangedListener(this); 2818 String oldSignature = mSignature; 2819 String bodyText = getBody().getText().toString(); 2820 if (!TextUtils.isEmpty(oldSignature)) { 2821 int pos = getSignatureStartPosition(oldSignature, bodyText); 2822 if (pos > -1) { 2823 mBodyView.setText(bodyText.substring(0, pos)); 2824 } 2825 } 2826 setAccount(mReplyFromAccount.account); 2827 mBodyView.addTextChangedListener(this); 2828 // TODO: handle discarding attachments when switching accounts. 2829 // Only enable save for this draft if there is any other content 2830 // in the message. 2831 if (!isBlank()) { 2832 enableSave(true); 2833 } 2834 mReplyFromChanged = true; 2835 initRecipients(); 2836 } 2837 } 2838 2839 public void enableSave(boolean enabled) { 2840 if (mSave != null) { 2841 mSave.setEnabled(enabled); 2842 } 2843 } 2844 2845 public void enableSend(boolean enabled) { 2846 if (mSend != null) { 2847 mSend.setEnabled(enabled); 2848 } 2849 } 2850 2851 /** 2852 * Handles button clicks from any error dialogs dealing with sending 2853 * a message. 2854 */ 2855 @Override 2856 public void onClick(DialogInterface dialog, int which) { 2857 switch (which) { 2858 case DialogInterface.BUTTON_POSITIVE: { 2859 doDiscardWithoutConfirmation(true /* show toast */ ); 2860 break; 2861 } 2862 case DialogInterface.BUTTON_NEGATIVE: { 2863 // If the user cancels the send, re-enable the send button. 2864 enableSend(true); 2865 break; 2866 } 2867 } 2868 2869 } 2870 2871 private void doDiscard() { 2872 new AlertDialog.Builder(this).setMessage(R.string.confirm_discard_text) 2873 .setPositiveButton(R.string.ok, this) 2874 .setNegativeButton(R.string.cancel, null) 2875 .create().show(); 2876 } 2877 /** 2878 * Effectively discard the current message. 2879 * 2880 * This method is either invoked from the menu or from the dialog 2881 * once the user has confirmed that they want to discard the message. 2882 * @param showToast show "Message discarded" toast if true 2883 */ 2884 private void doDiscardWithoutConfirmation(boolean showToast) { 2885 synchronized (mDraftLock) { 2886 if (mDraftId != UIProvider.INVALID_MESSAGE_ID) { 2887 ContentValues values = new ContentValues(); 2888 values.put(BaseColumns._ID, mDraftId); 2889 if (!mAccount.expungeMessageUri.equals(Uri.EMPTY)) { 2890 getContentResolver().update(mAccount.expungeMessageUri, values, null, null); 2891 } else { 2892 getContentResolver().delete(mDraft.uri, null, null); 2893 } 2894 // This is not strictly necessary (since we should not try to 2895 // save the draft after calling this) but it ensures that if we 2896 // do save again for some reason we make a new draft rather than 2897 // trying to resave an expunged draft. 2898 mDraftId = UIProvider.INVALID_MESSAGE_ID; 2899 } 2900 } 2901 2902 if (showToast) { 2903 // Display a toast to let the user know 2904 Toast.makeText(this, R.string.message_discarded, Toast.LENGTH_SHORT).show(); 2905 } 2906 2907 // This prevents the draft from being saved in onPause(). 2908 discardChanges(); 2909 finish(); 2910 } 2911 2912 private void saveIfNeeded() { 2913 if (mAccount == null) { 2914 // We have not chosen an account yet so there's no way that we can save. This is ok, 2915 // though, since we are saving our state before AccountsActivity is activated. Thus, the 2916 // user has not interacted with us yet and there is no real state to save. 2917 return; 2918 } 2919 2920 if (shouldSave()) { 2921 doSave(!mAddingAttachment /* show toast */); 2922 } 2923 } 2924 2925 @Override 2926 public void onAttachmentDeleted() { 2927 mAttachmentsChanged = true; 2928 // If we are showing any attachments, make sure we have an upper 2929 // divider. 2930 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0); 2931 updateSaveUi(); 2932 } 2933 2934 @Override 2935 public void onAttachmentAdded() { 2936 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0); 2937 mAttachmentsView.focusLastAttachment(); 2938 } 2939 2940 /** 2941 * This is called any time one of our text fields changes. 2942 */ 2943 @Override 2944 public void afterTextChanged(Editable s) { 2945 mTextChanged = true; 2946 updateSaveUi(); 2947 } 2948 2949 @Override 2950 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 2951 // Do nothing. 2952 } 2953 2954 @Override 2955 public void onTextChanged(CharSequence s, int start, int before, int count) { 2956 // Do nothing. 2957 } 2958 2959 2960 // There is a big difference between the text associated with an address changing 2961 // to add the display name or to format properly and a recipient being added or deleted. 2962 // Make sure we only notify of changes when a recipient has been added or deleted. 2963 private class RecipientTextWatcher implements TextWatcher { 2964 private HashMap<String, Integer> mContent = new HashMap<String, Integer>(); 2965 2966 private RecipientEditTextView mView; 2967 2968 private TextWatcher mListener; 2969 2970 public RecipientTextWatcher(RecipientEditTextView view, TextWatcher listener) { 2971 mView = view; 2972 mListener = listener; 2973 } 2974 2975 @Override 2976 public void afterTextChanged(Editable s) { 2977 if (hasChanged()) { 2978 mListener.afterTextChanged(s); 2979 } 2980 } 2981 2982 private boolean hasChanged() { 2983 String[] currRecips = tokenizeRecips(getAddressesFromList(mView)); 2984 int totalCount = currRecips.length; 2985 int totalPrevCount = 0; 2986 for (Entry<String, Integer> entry : mContent.entrySet()) { 2987 totalPrevCount += entry.getValue(); 2988 } 2989 if (totalCount != totalPrevCount) { 2990 return true; 2991 } 2992 2993 for (String recip : currRecips) { 2994 if (!mContent.containsKey(recip)) { 2995 return true; 2996 } else { 2997 int count = mContent.get(recip) - 1; 2998 if (count < 0) { 2999 return true; 3000 } else { 3001 mContent.put(recip, count); 3002 } 3003 } 3004 } 3005 return false; 3006 } 3007 3008 private String[] tokenizeRecips(String[] recips) { 3009 // Tokenize them all and put them in the list. 3010 String[] recipAddresses = new String[recips.length]; 3011 for (int i = 0; i < recips.length; i++) { 3012 recipAddresses[i] = Rfc822Tokenizer.tokenize(recips[i])[0].getAddress(); 3013 } 3014 return recipAddresses; 3015 } 3016 3017 @Override 3018 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 3019 String[] recips = tokenizeRecips(getAddressesFromList(mView)); 3020 for (String recip : recips) { 3021 if (!mContent.containsKey(recip)) { 3022 mContent.put(recip, 1); 3023 } else { 3024 mContent.put(recip, (mContent.get(recip)) + 1); 3025 } 3026 } 3027 } 3028 3029 @Override 3030 public void onTextChanged(CharSequence s, int start, int before, int count) { 3031 // Do nothing. 3032 } 3033 } 3034 3035 public static void registerTestSendOrSaveCallback(SendOrSaveCallback testCallback) { 3036 if (sTestSendOrSaveCallback != null && testCallback != null) { 3037 throw new IllegalStateException("Attempting to register more than one test callback"); 3038 } 3039 sTestSendOrSaveCallback = testCallback; 3040 } 3041 3042 @VisibleForTesting 3043 protected ArrayList<Attachment> getAttachments() { 3044 return mAttachmentsView.getAttachments(); 3045 } 3046 3047 @Override 3048 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 3049 switch (id) { 3050 case REFERENCE_MESSAGE_LOADER: 3051 return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null, 3052 null, null); 3053 case LOADER_ACCOUNT_CURSOR: 3054 return new CursorLoader(this, MailAppProvider.getAccountsUri(), 3055 UIProvider.ACCOUNTS_PROJECTION, null, null, null); 3056 } 3057 return null; 3058 } 3059 3060 @Override 3061 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 3062 int id = loader.getId(); 3063 switch (id) { 3064 case REFERENCE_MESSAGE_LOADER: 3065 if (data != null && data.moveToFirst()) { 3066 mRefMessage = new Message(data); 3067 Intent intent = getIntent(); 3068 int action = intent.getIntExtra(EXTRA_ACTION, COMPOSE); 3069 initFromRefMessage(action); 3070 finishSetup(action, intent, null, true); 3071 if (action != FORWARD) { 3072 String to = intent.getStringExtra(EXTRA_TO); 3073 if (!TextUtils.isEmpty(to)) { 3074 mRefMessage.setTo(null); 3075 mRefMessage.setFrom(null); 3076 clearChangeListeners(); 3077 mTo.append(to); 3078 initChangeListeners(); 3079 } 3080 } 3081 } else { 3082 finish(); 3083 } 3084 break; 3085 case LOADER_ACCOUNT_CURSOR: 3086 if (data != null && data.moveToFirst()) { 3087 // there are accounts now! 3088 Account account; 3089 final ArrayList<Account> accounts = new ArrayList<Account>(); 3090 final ArrayList<Account> initializedAccounts = new ArrayList<Account>(); 3091 do { 3092 account = new Account(data); 3093 if (account.isAccountReady()) { 3094 initializedAccounts.add(account); 3095 } 3096 accounts.add(account); 3097 } while (data.moveToNext()); 3098 if (initializedAccounts.size() > 0) { 3099 findViewById(R.id.wait).setVisibility(View.GONE); 3100 getLoaderManager().destroyLoader(LOADER_ACCOUNT_CURSOR); 3101 findViewById(R.id.compose).setVisibility(View.VISIBLE); 3102 mAccounts = initializedAccounts.toArray( 3103 new Account[initializedAccounts.size()]); 3104 3105 finishCreate(); 3106 invalidateOptionsMenu(); 3107 } else { 3108 // Show "waiting" 3109 account = accounts.size() > 0 ? accounts.get(0) : null; 3110 showWaitFragment(account); 3111 } 3112 } 3113 break; 3114 } 3115 } 3116 3117 private void showWaitFragment(Account account) { 3118 WaitFragment fragment = getWaitFragment(); 3119 if (fragment != null) { 3120 fragment.updateAccount(account); 3121 } else { 3122 findViewById(R.id.wait).setVisibility(View.VISIBLE); 3123 replaceFragment(WaitFragment.newInstance(account, true), 3124 FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_WAIT); 3125 } 3126 } 3127 3128 private WaitFragment getWaitFragment() { 3129 return (WaitFragment) getFragmentManager().findFragmentByTag(TAG_WAIT); 3130 } 3131 3132 private int replaceFragment(Fragment fragment, int transition, String tag) { 3133 FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction(); 3134 fragmentTransaction.addToBackStack(null); 3135 fragmentTransaction.setTransition(transition); 3136 fragmentTransaction.replace(R.id.wait, fragment, tag); 3137 final int transactionId = fragmentTransaction.commitAllowingStateLoss(); 3138 return transactionId; 3139 } 3140 3141 @Override 3142 public void onLoaderReset(Loader<Cursor> arg0) { 3143 // Do nothing. 3144 } 3145} 3146