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