MessageCompose.java revision 687f9962d7095e18ef994cd0e64337f02ed1a5bd
1/* 2 * Copyright (C) 2008 The Android Open Source Project 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.email.activity; 18 19import java.io.Serializable; 20import java.util.ArrayList; 21import java.util.Date; 22 23import android.app.Activity; 24import android.content.ActivityNotFoundException; 25import android.content.ContentResolver; 26import android.content.Context; 27import android.content.Intent; 28import android.content.pm.ActivityInfo; 29import android.database.Cursor; 30import android.net.Uri; 31import android.os.Bundle; 32import android.os.Handler; 33import android.os.Parcelable; 34import android.provider.OpenableColumns; 35import android.text.TextWatcher; 36import android.text.util.Rfc822Tokenizer; 37import android.util.Config; 38import android.util.Log; 39import android.view.Menu; 40import android.view.MenuItem; 41import android.view.View; 42import android.view.Window; 43import android.view.View.OnClickListener; 44import android.view.View.OnFocusChangeListener; 45import android.webkit.WebView; 46import android.widget.Button; 47import android.widget.EditText; 48import android.widget.ImageButton; 49import android.widget.LinearLayout; 50import android.widget.MultiAutoCompleteTextView; 51import android.widget.TextView; 52import android.widget.Toast; 53import android.widget.AutoCompleteTextView.Validator; 54 55import com.android.email.Account; 56import com.android.email.Email; 57import com.android.email.EmailAddressAdapter; 58import com.android.email.EmailAddressValidator; 59import com.android.email.MessagingController; 60import com.android.email.MessagingListener; 61import com.android.email.Preferences; 62import com.android.email.R; 63import com.android.email.Utility; 64import com.android.email.mail.Address; 65import com.android.email.mail.Body; 66import com.android.email.mail.Message; 67import com.android.email.mail.MessagingException; 68import com.android.email.mail.Multipart; 69import com.android.email.mail.Part; 70import com.android.email.mail.Message.RecipientType; 71import com.android.email.mail.internet.MimeBodyPart; 72import com.android.email.mail.internet.MimeHeader; 73import com.android.email.mail.internet.MimeMessage; 74import com.android.email.mail.internet.MimeMultipart; 75import com.android.email.mail.internet.MimeUtility; 76import com.android.email.mail.internet.TextBody; 77import com.android.email.mail.store.LocalStore; 78import com.android.email.mail.store.LocalStore.LocalAttachmentBody; 79 80public class MessageCompose extends Activity implements OnClickListener, OnFocusChangeListener { 81 private static final String ACTION_REPLY = "com.android.email.intent.action.REPLY"; 82 private static final String ACTION_REPLY_ALL = "com.android.email.intent.action.REPLY_ALL"; 83 private static final String ACTION_FORWARD = "com.android.email.intent.action.FORWARD"; 84 private static final String ACTION_EDIT_DRAFT = "com.android.email.intent.action.EDIT_DRAFT"; 85 86 private static final String EXTRA_ACCOUNT = "account"; 87 private static final String EXTRA_FOLDER = "folder"; 88 private static final String EXTRA_MESSAGE = "message"; 89 90 private static final String STATE_KEY_ATTACHMENTS = 91 "com.android.email.activity.MessageCompose.attachments"; 92 private static final String STATE_KEY_CC_SHOWN = 93 "com.android.email.activity.MessageCompose.ccShown"; 94 private static final String STATE_KEY_BCC_SHOWN = 95 "com.android.email.activity.MessageCompose.bccShown"; 96 private static final String STATE_KEY_QUOTED_TEXT_SHOWN = 97 "com.android.email.activity.MessageCompose.quotedTextShown"; 98 private static final String STATE_KEY_SOURCE_MESSAGE_PROCED = 99 "com.android.email.activity.MessageCompose.stateKeySourceMessageProced"; 100 private static final String STATE_KEY_DRAFT_UID = 101 "com.android.email.activity.MessageCompose.draftUid"; 102 103 private static final int MSG_PROGRESS_ON = 1; 104 private static final int MSG_PROGRESS_OFF = 2; 105 private static final int MSG_UPDATE_TITLE = 3; 106 private static final int MSG_SKIPPED_ATTACHMENTS = 4; 107 private static final int MSG_SAVED_DRAFT = 5; 108 private static final int MSG_DISCARDED_DRAFT = 6; 109 110 private static final int ACTIVITY_REQUEST_PICK_ATTACHMENT = 1; 111 112 private Account mAccount; 113 private String mFolder; 114 private String mSourceMessageUid; 115 private Message mSourceMessage; 116 /** 117 * Indicates that the source message has been processed at least once and should not 118 * be processed on any subsequent loads. This protects us from adding attachments that 119 * have already been added from the restore of the view state. 120 */ 121 private boolean mSourceMessageProcessed = false; 122 123 private MultiAutoCompleteTextView mToView; 124 private MultiAutoCompleteTextView mCcView; 125 private MultiAutoCompleteTextView mBccView; 126 private EditText mSubjectView; 127 private EditText mMessageContentView; 128 private Button mSendButton; 129 private Button mDiscardButton; 130 private Button mSaveButton; 131 private LinearLayout mAttachments; 132 private View mQuotedTextBar; 133 private ImageButton mQuotedTextDelete; 134 private WebView mQuotedText; 135 136 private boolean mDraftNeedsSaving = false; 137 138 /** 139 * The draft uid of this message. This is used when saving drafts so that the same draft is 140 * overwritten instead of being created anew. This property is null until the first save. 141 */ 142 private String mDraftUid; 143 144 private Handler mHandler = new Handler() { 145 @Override 146 public void handleMessage(android.os.Message msg) { 147 switch (msg.what) { 148 case MSG_PROGRESS_ON: 149 setProgressBarIndeterminateVisibility(true); 150 break; 151 case MSG_PROGRESS_OFF: 152 setProgressBarIndeterminateVisibility(false); 153 break; 154 case MSG_UPDATE_TITLE: 155 updateTitle(); 156 break; 157 case MSG_SKIPPED_ATTACHMENTS: 158 Toast.makeText( 159 MessageCompose.this, 160 getString(R.string.message_compose_attachments_skipped_toast), 161 Toast.LENGTH_LONG).show(); 162 break; 163 case MSG_SAVED_DRAFT: 164 Toast.makeText( 165 MessageCompose.this, 166 getString(R.string.message_saved_toast), 167 Toast.LENGTH_LONG).show(); 168 break; 169 case MSG_DISCARDED_DRAFT: 170 Toast.makeText( 171 MessageCompose.this, 172 getString(R.string.message_discarded_toast), 173 Toast.LENGTH_LONG).show(); 174 break; 175 default: 176 super.handleMessage(msg); 177 break; 178 } 179 } 180 }; 181 182 private Listener mListener = new Listener(); 183 private EmailAddressAdapter mAddressAdapter; 184 private Validator mAddressValidator; 185 186 /** 187 * Encapsulates known information about a single attachment. 188 */ 189 private static class Attachment implements Serializable { 190 public String name; 191 public String contentType; 192 public long size; 193 public Uri uri; 194 } 195 196 /** 197 * Compose a new message using the given account. If account is null the default account 198 * will be used. 199 * @param context 200 * @param account 201 */ 202 public static void actionCompose(Context context, Account account) { 203 try { 204 Intent i = new Intent(context, MessageCompose.class); 205 i.putExtra(EXTRA_ACCOUNT, account); 206 context.startActivity(i); 207 } catch (ActivityNotFoundException anfe) { 208 // Swallow it - this is usually a race condition, especially under automated test. 209 // (The message composer might have been disabled) 210 if (Config.LOGD) { 211 Log.d(Email.LOG_TAG, anfe.toString()); 212 } 213 } 214 } 215 216 /** 217 * Compose a new message as a reply to the given message. If replyAll is true the function 218 * is reply all instead of simply reply. 219 * @param context 220 * @param account 221 * @param message 222 * @param replyAll 223 */ 224 public static void actionReply( 225 Context context, 226 Account account, 227 Message message, 228 boolean replyAll) { 229 Intent i = new Intent(context, MessageCompose.class); 230 i.putExtra(EXTRA_ACCOUNT, account); 231 i.putExtra(EXTRA_FOLDER, message.getFolder().getName()); 232 i.putExtra(EXTRA_MESSAGE, message.getUid()); 233 if (replyAll) { 234 i.setAction(ACTION_REPLY_ALL); 235 } 236 else { 237 i.setAction(ACTION_REPLY); 238 } 239 context.startActivity(i); 240 } 241 242 /** 243 * Compose a new message as a forward of the given message. 244 * @param context 245 * @param account 246 * @param message 247 */ 248 public static void actionForward(Context context, Account account, Message message) { 249 Intent i = new Intent(context, MessageCompose.class); 250 i.putExtra(EXTRA_ACCOUNT, account); 251 i.putExtra(EXTRA_FOLDER, message.getFolder().getName()); 252 i.putExtra(EXTRA_MESSAGE, message.getUid()); 253 i.setAction(ACTION_FORWARD); 254 context.startActivity(i); 255 } 256 257 /** 258 * Continue composition of the given message. This action modifies the way this Activity 259 * handles certain actions. 260 * Save will attempt to replace the message in the given folder with the updated version. 261 * Discard will delete the message from the given folder. 262 * @param context 263 * @param account 264 * @param message 265 */ 266 public static void actionEditDraft(Context context, Account account, Message message) { 267 Intent i = new Intent(context, MessageCompose.class); 268 i.putExtra(EXTRA_ACCOUNT, account); 269 i.putExtra(EXTRA_FOLDER, message.getFolder().getName()); 270 i.putExtra(EXTRA_MESSAGE, message.getUid()); 271 i.setAction(ACTION_EDIT_DRAFT); 272 context.startActivity(i); 273 } 274 275 @Override 276 public void onCreate(Bundle savedInstanceState) { 277 super.onCreate(savedInstanceState); 278 279 requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); 280 281 setContentView(R.layout.message_compose); 282 283 mAddressAdapter = new EmailAddressAdapter(this); 284 mAddressValidator = new EmailAddressValidator(); 285 286 mToView = (MultiAutoCompleteTextView)findViewById(R.id.to); 287 mCcView = (MultiAutoCompleteTextView)findViewById(R.id.cc); 288 mBccView = (MultiAutoCompleteTextView)findViewById(R.id.bcc); 289 mSubjectView = (EditText)findViewById(R.id.subject); 290 mMessageContentView = (EditText)findViewById(R.id.message_content); 291 mSendButton = (Button)findViewById(R.id.send); 292 mDiscardButton = (Button)findViewById(R.id.discard); 293 mSaveButton = (Button)findViewById(R.id.save); 294 mAttachments = (LinearLayout)findViewById(R.id.attachments); 295 mQuotedTextBar = findViewById(R.id.quoted_text_bar); 296 mQuotedTextDelete = (ImageButton)findViewById(R.id.quoted_text_delete); 297 mQuotedText = (WebView)findViewById(R.id.quoted_text); 298 299 TextWatcher watcher = new TextWatcher() { 300 public void beforeTextChanged(CharSequence s, int start, 301 int before, int after) { } 302 303 public void onTextChanged(CharSequence s, int start, 304 int before, int count) { 305 mDraftNeedsSaving = true; 306 } 307 308 public void afterTextChanged(android.text.Editable s) { } 309 }; 310 311 mToView.addTextChangedListener(watcher); 312 mCcView.addTextChangedListener(watcher); 313 mBccView.addTextChangedListener(watcher); 314 mSubjectView.addTextChangedListener(watcher); 315 mMessageContentView.addTextChangedListener(watcher); 316 317 /* 318 * We set this to invisible by default. Other methods will turn it back on if it's 319 * needed. 320 */ 321 mQuotedTextBar.setVisibility(View.GONE); 322 mQuotedText.setVisibility(View.GONE); 323 324 mQuotedTextDelete.setOnClickListener(this); 325 326 mToView.setAdapter(mAddressAdapter); 327 mToView.setTokenizer(new Rfc822Tokenizer()); 328 mToView.setValidator(mAddressValidator); 329 330 mCcView.setAdapter(mAddressAdapter); 331 mCcView.setTokenizer(new Rfc822Tokenizer()); 332 mCcView.setValidator(mAddressValidator); 333 334 mBccView.setAdapter(mAddressAdapter); 335 mBccView.setTokenizer(new Rfc822Tokenizer()); 336 mBccView.setValidator(mAddressValidator); 337 338 mSendButton.setOnClickListener(this); 339 mDiscardButton.setOnClickListener(this); 340 mSaveButton.setOnClickListener(this); 341 342 mSubjectView.setOnFocusChangeListener(this); 343 344 if (savedInstanceState != null) { 345 /* 346 * This data gets used in onCreate, so grab it here instead of onRestoreIntstanceState 347 */ 348 mSourceMessageProcessed = 349 savedInstanceState.getBoolean(STATE_KEY_SOURCE_MESSAGE_PROCED, false); 350 } 351 352 Intent intent = getIntent(); 353 354 String action = intent.getAction(); 355 356 if (Intent.ACTION_VIEW.equals(action) || Intent.ACTION_SENDTO.equals(action)) { 357 /* 358 * Someone has clicked a mailto: link. The address is in the URI. 359 */ 360 mAccount = Preferences.getPreferences(this).getDefaultAccount(); 361 if (mAccount == null) { 362 /* 363 * There are no accounts set up. This should not have happened. Prompt the 364 * user to set up an account as an acceptable bailout. 365 */ 366 Accounts.actionShowAccounts(this); 367 mDraftNeedsSaving = false; 368 finish(); 369 return; 370 } 371 if (intent.getData() != null) { 372 Uri uri = intent.getData(); 373 try { 374 if (uri.getScheme().equalsIgnoreCase("mailto")) { 375 Address[] addresses = Address.parse(uri.getSchemeSpecificPart()); 376 addAddresses(mToView, addresses); 377 } 378 } 379 catch (Exception e) { 380 /* 381 * If we can't extract any information from the URI it's okay. They can 382 * still compose a message. 383 */ 384 } 385 } 386 } 387 else if (Intent.ACTION_SEND.equals(action)) { 388 /* 389 * Someone is trying to compose an email with an attachment, probably Pictures. 390 * The Intent should contain an EXTRA_STREAM with the data to attach. 391 */ 392 393 mAccount = Preferences.getPreferences(this).getDefaultAccount(); 394 if (mAccount == null) { 395 /* 396 * There are no accounts set up. This should not have happened. Prompt the 397 * user to set up an account as an acceptable bailout. 398 */ 399 Accounts.actionShowAccounts(this); 400 mDraftNeedsSaving = false; 401 finish(); 402 return; 403 } 404 405 String type = intent.getType(); 406 Uri stream = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM); 407 if (stream != null && type != null) { 408 if (MimeUtility.mimeTypeMatches(type, Email.ACCEPTABLE_ATTACHMENT_SEND_TYPES)) { 409 addAttachment(stream); 410 } 411 } 412 } 413 else { 414 mAccount = (Account) intent.getSerializableExtra(EXTRA_ACCOUNT); 415 mFolder = intent.getStringExtra(EXTRA_FOLDER); 416 mSourceMessageUid = intent.getStringExtra(EXTRA_MESSAGE); 417 } 418 419 if (ACTION_REPLY.equals(action) || ACTION_REPLY_ALL.equals(action) || 420 ACTION_FORWARD.equals(action) || ACTION_EDIT_DRAFT.equals(action)) { 421 /* 422 * If we need to load the message we add ourself as a message listener here 423 * so we can kick it off. Normally we add in onResume but we don't 424 * want to reload the message every time the activity is resumed. 425 * There is no harm in adding twice. 426 */ 427 MessagingController.getInstance(getApplication()).addListener(mListener); 428 MessagingController.getInstance(getApplication()).loadMessageForView( 429 mAccount, 430 mFolder, 431 mSourceMessageUid, 432 mListener); 433 } 434 435 updateTitle(); 436 } 437 438 @Override 439 public void onResume() { 440 super.onResume(); 441 MessagingController.getInstance(getApplication()).addListener(mListener); 442 } 443 444 @Override 445 public void onPause() { 446 super.onPause(); 447 saveIfNeeded(); 448 MessagingController.getInstance(getApplication()).removeListener(mListener); 449 } 450 451 /** 452 * We override onDestroy to make sure that the WebView gets explicitly destroyed. 453 * Otherwise it can leak native references. 454 */ 455 @Override 456 public void onDestroy() { 457 super.onDestroy(); 458 mQuotedText.destroy(); 459 mQuotedText = null; 460 } 461 462 /** 463 * The framework handles most of the fields, but we need to handle stuff that we 464 * dynamically show and hide: 465 * Attachment list, 466 * Cc field, 467 * Bcc field, 468 * Quoted text, 469 */ 470 @Override 471 protected void onSaveInstanceState(Bundle outState) { 472 super.onSaveInstanceState(outState); 473 saveIfNeeded(); 474 ArrayList<Uri> attachments = new ArrayList<Uri>(); 475 for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) { 476 View view = mAttachments.getChildAt(i); 477 Attachment attachment = (Attachment) view.getTag(); 478 attachments.add(attachment.uri); 479 } 480 outState.putParcelableArrayList(STATE_KEY_ATTACHMENTS, attachments); 481 outState.putBoolean(STATE_KEY_CC_SHOWN, mCcView.getVisibility() == View.VISIBLE); 482 outState.putBoolean(STATE_KEY_BCC_SHOWN, mBccView.getVisibility() == View.VISIBLE); 483 outState.putBoolean(STATE_KEY_QUOTED_TEXT_SHOWN, 484 mQuotedTextBar.getVisibility() == View.VISIBLE); 485 outState.putBoolean(STATE_KEY_SOURCE_MESSAGE_PROCED, mSourceMessageProcessed); 486 outState.putString(STATE_KEY_DRAFT_UID, mDraftUid); 487 } 488 489 @Override 490 protected void onRestoreInstanceState(Bundle savedInstanceState) { 491 super.onRestoreInstanceState(savedInstanceState); 492 ArrayList<Parcelable> attachments = 493 savedInstanceState.getParcelableArrayList(STATE_KEY_ATTACHMENTS); 494 mAttachments.removeAllViews(); 495 for (Parcelable p : attachments) { 496 Uri uri = (Uri) p; 497 addAttachment(uri); 498 } 499 500 mCcView.setVisibility(savedInstanceState.getBoolean(STATE_KEY_CC_SHOWN) ? 501 View.VISIBLE : View.GONE); 502 mBccView.setVisibility(savedInstanceState.getBoolean(STATE_KEY_BCC_SHOWN) ? 503 View.VISIBLE : View.GONE); 504 mQuotedTextBar.setVisibility(savedInstanceState.getBoolean(STATE_KEY_QUOTED_TEXT_SHOWN) ? 505 View.VISIBLE : View.GONE); 506 mQuotedText.setVisibility(savedInstanceState.getBoolean(STATE_KEY_QUOTED_TEXT_SHOWN) ? 507 View.VISIBLE : View.GONE); 508 mDraftUid = savedInstanceState.getString(STATE_KEY_DRAFT_UID); 509 mDraftNeedsSaving = false; 510 } 511 512 private void updateTitle() { 513 if (mSubjectView.getText().length() == 0) { 514 setTitle(R.string.compose_title); 515 } else { 516 setTitle(mSubjectView.getText().toString()); 517 } 518 } 519 520 public void onFocusChange(View view, boolean focused) { 521 if (!focused) { 522 updateTitle(); 523 } 524 } 525 526 private void addAddresses(MultiAutoCompleteTextView view, Address[] addresses) { 527 if (addresses == null) { 528 return; 529 } 530 for (Address address : addresses) { 531 addAddress(view, address); 532 } 533 } 534 535 private void addAddress(MultiAutoCompleteTextView view, Address address) { 536 view.append(address + ", "); 537 } 538 539 private Address[] getAddresses(MultiAutoCompleteTextView view) { 540 Address[] addresses = Address.parse(view.getText().toString().trim()); 541 return addresses; 542 } 543 544 private MimeMessage createMessage() throws MessagingException { 545 MimeMessage message = new MimeMessage(); 546 message.setSentDate(new Date()); 547 Address from = new Address(mAccount.getEmail(), mAccount.getName()); 548 message.setFrom(from); 549 message.setRecipients(RecipientType.TO, getAddresses(mToView)); 550 message.setRecipients(RecipientType.CC, getAddresses(mCcView)); 551 message.setRecipients(RecipientType.BCC, getAddresses(mBccView)); 552 message.setSubject(mSubjectView.getText().toString()); 553 554 /* 555 * Build the Body that will contain the text of the message. We'll decide where to 556 * include it later. 557 */ 558 559 String text = mMessageContentView.getText().toString(); 560 561 if (mQuotedTextBar.getVisibility() == View.VISIBLE) { 562 String action = getIntent().getAction(); 563 String quotedText = null; 564 Part part = MimeUtility.findFirstPartByMimeType(mSourceMessage, 565 "text/plain"); 566 if (part != null) { 567 quotedText = MimeUtility.getTextFromPart(part); 568 } 569 if (ACTION_REPLY.equals(action) || ACTION_REPLY_ALL.equals(action)) { 570 text += String.format( 571 getString(R.string.message_compose_reply_header_fmt), 572 Address.toString(mSourceMessage.getFrom())); 573 if (quotedText != null) { 574 text += quotedText.replaceAll("(?m)^", ">"); 575 } 576 } 577 else if (ACTION_FORWARD.equals(action)) { 578 text += String.format( 579 getString(R.string.message_compose_fwd_header_fmt), 580 mSourceMessage.getSubject(), 581 Address.toString(mSourceMessage.getFrom()), 582 Address.toString( 583 mSourceMessage.getRecipients(RecipientType.TO)), 584 Address.toString( 585 mSourceMessage.getRecipients(RecipientType.CC))); 586 if (quotedText != null) { 587 text += quotedText; 588 } 589 } 590 } 591 592 TextBody body = new TextBody(text); 593 594 if (mAttachments.getChildCount() > 0) { 595 /* 596 * The message has attachments that need to be included. First we add the part 597 * containing the text that will be sent and then we include each attachment. 598 */ 599 600 MimeMultipart mp; 601 602 mp = new MimeMultipart(); 603 mp.addBodyPart(new MimeBodyPart(body, "text/plain")); 604 605 for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) { 606 Attachment attachment = (Attachment) mAttachments.getChildAt(i).getTag(); 607 MimeBodyPart bp = new MimeBodyPart( 608 new LocalStore.LocalAttachmentBody(attachment.uri, getApplication())); 609 bp.setHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\n name=\"%s\"", 610 attachment.contentType, 611 attachment.name)); 612 bp.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64"); 613 bp.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, 614 String.format("attachment;\n filename=\"%s\"", 615 attachment.name)); 616 mp.addBodyPart(bp); 617 } 618 619 message.setBody(mp); 620 } 621 else { 622 /* 623 * No attachments to include, just stick the text body in the message and call 624 * it good. 625 */ 626 message.setBody(body); 627 } 628 629 return message; 630 } 631 632 private void sendOrSaveMessage(boolean save) { 633 /* 634 * Create the message from all the data the user has entered. 635 */ 636 MimeMessage message; 637 try { 638 message = createMessage(); 639 } 640 catch (MessagingException me) { 641 Log.e(Email.LOG_TAG, "Failed to create new message for send or save.", me); 642 throw new RuntimeException("Failed to create a new message for send or save.", me); 643 } 644 645 if (save) { 646 /* 647 * Save a draft 648 */ 649 if (mDraftUid != null) { 650 message.setUid(mDraftUid); 651 } 652 else if (ACTION_EDIT_DRAFT.equals(getIntent().getAction())) { 653 /* 654 * We're saving a previously saved draft, so update the new message's uid 655 * to the old message's uid. 656 */ 657 message.setUid(mSourceMessageUid); 658 } 659 MessagingController.getInstance(getApplication()).saveDraft(mAccount, message); 660 mDraftUid = message.getUid(); 661 662 // Don't display the toast if the user is just changing the orientation 663 if ((getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) { 664 mHandler.sendEmptyMessage(MSG_SAVED_DRAFT); 665 } 666 } 667 else { 668 /* 669 * Send the message 670 * TODO Is it possible for us to be editing a draft with a null source message? Don't 671 * think so. Could probably remove below check. 672 */ 673 if (ACTION_EDIT_DRAFT.equals(getIntent().getAction()) 674 && mSourceMessageUid != null) { 675 /* 676 * We're sending a previously saved draft, so delete the old draft first. 677 */ 678 MessagingController.getInstance(getApplication()).deleteMessage( 679 mAccount, 680 mFolder, 681 mSourceMessage, 682 null); 683 } 684 MessagingController.getInstance(getApplication()).sendMessage(mAccount, message, null); 685 } 686 } 687 688 private void saveIfNeeded() { 689 if (!mDraftNeedsSaving) { 690 return; 691 } 692 mDraftNeedsSaving = false; 693 sendOrSaveMessage(true); 694 } 695 696 private void onSend() { 697 if (getAddresses(mToView).length == 0 && 698 getAddresses(mCcView).length == 0 && 699 getAddresses(mBccView).length == 0) { 700 mToView.setError(getString(R.string.message_compose_error_no_recipients)); 701 Toast.makeText(this, getString(R.string.message_compose_error_no_recipients), 702 Toast.LENGTH_LONG).show(); 703 return; 704 } 705 sendOrSaveMessage(false); 706 mDraftNeedsSaving = false; 707 finish(); 708 } 709 710 private void onDiscard() { 711 if (mSourceMessageUid != null) { 712 if (ACTION_EDIT_DRAFT.equals(getIntent().getAction()) && mSourceMessageUid != null) { 713 MessagingController.getInstance(getApplication()).deleteMessage( 714 mAccount, 715 mFolder, 716 mSourceMessage, 717 null); 718 } 719 } 720 mHandler.sendEmptyMessage(MSG_DISCARDED_DRAFT); 721 mDraftNeedsSaving = false; 722 finish(); 723 } 724 725 private void onSave() { 726 saveIfNeeded(); 727 finish(); 728 } 729 730 private void onAddCcBcc() { 731 mCcView.setVisibility(View.VISIBLE); 732 mBccView.setVisibility(View.VISIBLE); 733 } 734 735 /** 736 * Kick off a picker for whatever kind of MIME types we'll accept and let Android take over. 737 */ 738 private void onAddAttachment() { 739 Intent i = new Intent(Intent.ACTION_GET_CONTENT); 740 i.addCategory(Intent.CATEGORY_OPENABLE); 741 i.setType(Email.ACCEPTABLE_ATTACHMENT_SEND_TYPES[0]); 742 startActivityForResult(Intent.createChooser(i, null), ACTIVITY_REQUEST_PICK_ATTACHMENT); 743 } 744 745 private void addAttachment(Uri uri) { 746 addAttachment(uri, -1, null); 747 } 748 749 private void addAttachment(Uri uri, int size, String name) { 750 ContentResolver contentResolver = getContentResolver(); 751 752 String contentType = contentResolver.getType(uri); 753 754 if (contentType == null) { 755 contentType = ""; 756 } 757 758 Attachment attachment = new Attachment(); 759 attachment.name = name; 760 attachment.contentType = contentType; 761 attachment.size = size; 762 attachment.uri = uri; 763 764 if (attachment.size == -1 || attachment.name == null) { 765 Cursor metadataCursor = contentResolver.query( 766 uri, 767 new String[]{ OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE }, 768 null, 769 null, 770 null); 771 if (metadataCursor != null) { 772 try { 773 if (metadataCursor.moveToFirst()) { 774 if (attachment.name == null) { 775 attachment.name = metadataCursor.getString(0); 776 } 777 if (attachment.size == -1) { 778 attachment.size = metadataCursor.getInt(1); 779 } 780 } 781 } finally { 782 metadataCursor.close(); 783 } 784 } 785 } 786 787 if (attachment.name == null) { 788 attachment.name = uri.getLastPathSegment(); 789 } 790 791 View view = getLayoutInflater().inflate( 792 R.layout.message_compose_attachment, 793 mAttachments, 794 false); 795 TextView nameView = (TextView)view.findViewById(R.id.attachment_name); 796 ImageButton delete = (ImageButton)view.findViewById(R.id.attachment_delete); 797 nameView.setText(attachment.name); 798 delete.setOnClickListener(this); 799 delete.setTag(view); 800 view.setTag(attachment); 801 mAttachments.addView(view); 802 } 803 804 @Override 805 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 806 if (data == null) { 807 return; 808 } 809 addAttachment(data.getData()); 810 mDraftNeedsSaving = true; 811 } 812 813 public void onClick(View view) { 814 switch (view.getId()) { 815 case R.id.send: 816 onSend(); 817 break; 818 case R.id.save: 819 onSave(); 820 break; 821 case R.id.discard: 822 onDiscard(); 823 break; 824 case R.id.attachment_delete: 825 /* 826 * The view is the delete button, and we have previously set the tag of 827 * the delete button to the view that owns it. We don't use parent because the 828 * view is very complex and could change in the future. 829 */ 830 mAttachments.removeView((View) view.getTag()); 831 mDraftNeedsSaving = true; 832 break; 833 case R.id.quoted_text_delete: 834 mQuotedTextBar.setVisibility(View.GONE); 835 mQuotedText.setVisibility(View.GONE); 836 mDraftNeedsSaving = true; 837 break; 838 } 839 } 840 841 @Override 842 public boolean onOptionsItemSelected(MenuItem item) { 843 switch (item.getItemId()) { 844 case R.id.send: 845 onSend(); 846 break; 847 case R.id.save: 848 onSave(); 849 break; 850 case R.id.discard: 851 onDiscard(); 852 break; 853 case R.id.add_cc_bcc: 854 onAddCcBcc(); 855 break; 856 case R.id.add_attachment: 857 onAddAttachment(); 858 break; 859 default: 860 return super.onOptionsItemSelected(item); 861 } 862 return true; 863 } 864 865 @Override 866 public boolean onCreateOptionsMenu(Menu menu) { 867 super.onCreateOptionsMenu(menu); 868 getMenuInflater().inflate(R.menu.message_compose_option, menu); 869 return true; 870 } 871 872 /** 873 * Returns true if all attachments were able to be attached, otherwise returns false. 874 */ 875 private boolean loadAttachments(Part part, int depth) throws MessagingException { 876 if (part.getBody() instanceof Multipart) { 877 Multipart mp = (Multipart) part.getBody(); 878 boolean ret = true; 879 for (int i = 0, count = mp.getCount(); i < count; i++) { 880 if (!loadAttachments(mp.getBodyPart(i), depth + 1)) { 881 ret = false; 882 } 883 } 884 return ret; 885 } else { 886 String contentType = MimeUtility.unfoldAndDecode(part.getContentType()); 887 String name = MimeUtility.getHeaderParameter(contentType, "name"); 888 if (name != null) { 889 Body body = part.getBody(); 890 if (body != null && body instanceof LocalAttachmentBody) { 891 final Uri uri = ((LocalAttachmentBody) body).getContentUri(); 892 mHandler.post(new Runnable() { 893 public void run() { 894 addAttachment(uri); 895 } 896 }); 897 } 898 else { 899 return false; 900 } 901 } 902 return true; 903 } 904 } 905 906 /** 907 * Pull out the parts of the now loaded source message and apply them to the new message 908 * depending on the type of message being composed. 909 * @param message 910 */ 911 /* package */ void processSourceMessage(Message message) { 912 String action = getIntent().getAction(); 913 if (ACTION_REPLY.equals(action) || ACTION_REPLY_ALL.equals(action)) { 914 try { 915 if (message.getSubject() != null && 916 !message.getSubject().toLowerCase().startsWith("re:")) { 917 mSubjectView.setText("Re: " + message.getSubject()); 918 } 919 else { 920 mSubjectView.setText(message.getSubject()); 921 } 922 /* 923 * If a reply-to was included with the message use that, otherwise use the from 924 * or sender address. 925 */ 926 Address[] replyToAddresses; 927 if (message.getReplyTo().length > 0) { 928 addAddresses(mToView, replyToAddresses = message.getReplyTo()); 929 } 930 else { 931 addAddresses(mToView, replyToAddresses = message.getFrom()); 932 } 933 if (ACTION_REPLY_ALL.equals(action)) { 934 for (Address address : message.getRecipients(RecipientType.TO)) { 935 if (!address.getAddress().equalsIgnoreCase(mAccount.getEmail())) { 936 addAddress(mToView, address); 937 } 938 } 939 if (message.getRecipients(RecipientType.CC).length > 0) { 940 for (Address address : message.getRecipients(RecipientType.CC)) { 941 if (!Utility.arrayContains(replyToAddresses, address)) { 942 addAddress(mCcView, address); 943 } 944 } 945 mCcView.setVisibility(View.VISIBLE); 946 } 947 } 948 949 Part part = MimeUtility.findFirstPartByMimeType(message, "text/plain"); 950 if (part == null) { 951 part = MimeUtility.findFirstPartByMimeType(message, "text/html"); 952 } 953 if (part != null) { 954 String text = MimeUtility.getTextFromPart(part); 955 if (text != null) { 956 mQuotedTextBar.setVisibility(View.VISIBLE); 957 mQuotedText.setVisibility(View.VISIBLE); 958 mQuotedText.loadDataWithBaseURL("email://", text, part.getMimeType(), 959 "utf-8", null); 960 } 961 } 962 } 963 catch (MessagingException me) { 964 /* 965 * This really should not happen at this point but if it does it's okay. 966 * The user can continue composing their message. 967 */ 968 } 969 } 970 else if (ACTION_FORWARD.equals(action)) { 971 try { 972 if (message.getSubject() != null && 973 !message.getSubject().toLowerCase().startsWith("fwd:")) { 974 mSubjectView.setText("Fwd: " + message.getSubject()); 975 } 976 else { 977 mSubjectView.setText(message.getSubject()); 978 } 979 980 Part part = MimeUtility.findFirstPartByMimeType(message, "text/plain"); 981 if (part == null) { 982 part = MimeUtility.findFirstPartByMimeType(message, "text/html"); 983 } 984 if (part != null) { 985 String text = MimeUtility.getTextFromPart(part); 986 if (text != null) { 987 mQuotedTextBar.setVisibility(View.VISIBLE); 988 mQuotedText.setVisibility(View.VISIBLE); 989 mQuotedText.loadDataWithBaseURL("email://", text, part.getMimeType(), 990 "utf-8", null); 991 } 992 } 993 if (!mSourceMessageProcessed) { 994 if (!loadAttachments(message, 0)) { 995 mHandler.sendEmptyMessage(MSG_SKIPPED_ATTACHMENTS); 996 } 997 } 998 } 999 catch (MessagingException me) { 1000 /* 1001 * This really should not happen at this point but if it does it's okay. 1002 * The user can continue composing their message. 1003 */ 1004 } 1005 } 1006 else if (ACTION_EDIT_DRAFT.equals(action)) { 1007 try { 1008 mSubjectView.setText(message.getSubject()); 1009 addAddresses(mToView, message.getRecipients(RecipientType.TO)); 1010 if (message.getRecipients(RecipientType.CC).length > 0) { 1011 addAddresses(mCcView, message.getRecipients(RecipientType.CC)); 1012 mCcView.setVisibility(View.VISIBLE); 1013 } 1014 if (message.getRecipients(RecipientType.BCC).length > 0) { 1015 addAddresses(mBccView, message.getRecipients(RecipientType.BCC)); 1016 mBccView.setVisibility(View.VISIBLE); 1017 } 1018 Part part = MimeUtility.findFirstPartByMimeType(message, "text/plain"); 1019 if (part != null) { 1020 String text = MimeUtility.getTextFromPart(part); 1021 mMessageContentView.setText(text); 1022 } 1023 if (!mSourceMessageProcessed) { 1024 loadAttachments(message, 0); 1025 } 1026 } 1027 catch (MessagingException me) { 1028 // TODO 1029 } 1030 } 1031 1032 // In order to accelerate typing, position the cursor in the first empty field, 1033 // or at the end of the body composition field if none are empty. Typically, this will 1034 // play out as follows: 1035 // Reply / Reply All - put cursor in the empty message body 1036 // Forward - put cursor in the empty To field 1037 // Edit Draft - put cursor in whatever field still needs entry 1038 if (mToView.length() == 0) { 1039 mToView.requestFocus(); 1040 } else if (mSubjectView.length() == 0) { 1041 mSubjectView.requestFocus(); 1042 } else { 1043 mMessageContentView.requestFocus(); 1044 // when selecting the message content, explicitly move IP to the end, so you can 1045 // quickly resume typing into a draft 1046 int selection = mMessageContentView.length(); 1047 mMessageContentView.setSelection(selection, selection); 1048 } 1049 1050 mSourceMessageProcessed = true; 1051 mDraftNeedsSaving = false; 1052 } 1053 1054 class Listener extends MessagingListener { 1055 @Override 1056 public void loadMessageForViewStarted(Account account, String folder, String uid) { 1057 mHandler.sendEmptyMessage(MSG_PROGRESS_ON); 1058 } 1059 1060 @Override 1061 public void loadMessageForViewFinished(Account account, String folder, String uid, 1062 Message message) { 1063 mHandler.sendEmptyMessage(MSG_PROGRESS_OFF); 1064 } 1065 1066 @Override 1067 public void loadMessageForViewBodyAvailable(Account account, String folder, String uid, 1068 final Message message) { 1069 mSourceMessage = message; 1070 runOnUiThread(new Runnable() { 1071 public void run() { 1072 processSourceMessage(message); 1073 } 1074 }); 1075 } 1076 1077 @Override 1078 public void loadMessageForViewFailed(Account account, String folder, String uid, 1079 final String message) { 1080 mHandler.sendEmptyMessage(MSG_PROGRESS_OFF); 1081 // TODO show network error 1082 } 1083 1084 @Override 1085 public void messageUidChanged( 1086 Account account, 1087 String folder, 1088 String oldUid, 1089 String newUid) { 1090 if (account.equals(mAccount) 1091 && (folder.equals(mFolder) 1092 || (mFolder == null 1093 && folder.equals(mAccount.getDraftsFolderName())))) { 1094 if (oldUid.equals(mDraftUid)) { 1095 mDraftUid = newUid; 1096 } 1097 if (oldUid.equals(mSourceMessageUid)) { 1098 mSourceMessageUid = newUid; 1099 } 1100 if (mSourceMessage != null && (oldUid.equals(mSourceMessage.getUid()))) { 1101 mSourceMessage.setUid(newUid); 1102 } 1103 } 1104 } 1105 } 1106} 1107