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