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