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