MessageCompose.java revision e3e65ed9195f2258c8a5eb452690e60c70461940
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 com.android.email.Controller; 20import com.android.email.Email; 21import com.android.email.EmailAddressAdapter; 22import com.android.email.EmailAddressValidator; 23import com.android.email.R; 24import com.android.email.Utility; 25import com.android.email.mail.Address; 26import com.android.email.mail.MessagingException; 27import com.android.email.mail.internet.EmailHtmlUtil; 28import com.android.email.mail.internet.MimeUtility; 29import com.android.email.provider.EmailContent; 30import com.android.email.provider.EmailContent.Account; 31import com.android.email.provider.EmailContent.Attachment; 32import com.android.email.provider.EmailContent.Body; 33import com.android.email.provider.EmailContent.BodyColumns; 34import com.android.email.provider.EmailContent.Message; 35import com.android.email.provider.EmailContent.MessageColumns; 36 37import android.app.Activity; 38import android.content.ActivityNotFoundException; 39import android.content.ContentResolver; 40import android.content.ContentUris; 41import android.content.ContentValues; 42import android.content.Context; 43import android.content.Intent; 44import android.content.pm.ActivityInfo; 45import android.database.Cursor; 46import android.net.Uri; 47import android.os.AsyncTask; 48import android.os.Bundle; 49import android.os.Handler; 50import android.os.Parcelable; 51import android.provider.OpenableColumns; 52import android.text.InputFilter; 53import android.text.SpannableStringBuilder; 54import android.text.Spanned; 55import android.text.TextWatcher; 56import android.text.util.Rfc822Tokenizer; 57import android.util.Log; 58import android.view.Menu; 59import android.view.MenuItem; 60import android.view.View; 61import android.view.Window; 62import android.view.View.OnClickListener; 63import android.view.View.OnFocusChangeListener; 64import android.webkit.WebView; 65import android.widget.Button; 66import android.widget.EditText; 67import android.widget.ImageButton; 68import android.widget.LinearLayout; 69import android.widget.MultiAutoCompleteTextView; 70import android.widget.TextView; 71import android.widget.Toast; 72 73import java.io.UnsupportedEncodingException; 74import java.net.URLDecoder; 75import java.util.ArrayList; 76import java.util.List; 77 78 79public class MessageCompose extends Activity implements OnClickListener, OnFocusChangeListener { 80 private static final String ACTION_REPLY = "com.android.email.intent.action.REPLY"; 81 private static final String ACTION_REPLY_ALL = "com.android.email.intent.action.REPLY_ALL"; 82 private static final String ACTION_FORWARD = "com.android.email.intent.action.FORWARD"; 83 private static final String ACTION_EDIT_DRAFT = "com.android.email.intent.action.EDIT_DRAFT"; 84 85 private static final String EXTRA_ACCOUNT_ID = "account_id"; 86 private static final String EXTRA_MESSAGE_ID = "message_id"; 87 private static final String STATE_KEY_CC_SHOWN = 88 "com.android.email.activity.MessageCompose.ccShown"; 89 private static final String STATE_KEY_BCC_SHOWN = 90 "com.android.email.activity.MessageCompose.bccShown"; 91 private static final String STATE_KEY_QUOTED_TEXT_SHOWN = 92 "com.android.email.activity.MessageCompose.quotedTextShown"; 93 private static final String STATE_KEY_SOURCE_MESSAGE_PROCED = 94 "com.android.email.activity.MessageCompose.stateKeySourceMessageProced"; 95 private static final String STATE_KEY_DRAFT_ID = 96 "com.android.email.activity.MessageCompose.draftId"; 97 98 private static final int MSG_UPDATE_TITLE = 3; 99 private static final int MSG_SKIPPED_ATTACHMENTS = 4; 100 private static final int MSG_DISCARDED_DRAFT = 6; 101 102 private static final int ACTIVITY_REQUEST_PICK_ATTACHMENT = 1; 103 104 private static final String[] ATTACHMENT_META_COLUMNS = { 105 OpenableColumns.DISPLAY_NAME, 106 OpenableColumns.SIZE 107 }; 108 109 private Account mAccount; 110 111 // mDraft has mId > 0 after the first draft save. 112 private Message mDraft = new Message(); 113 114 // mSource is only set for REPLY, REPLY_ALL and FORWARD, and contains the source message. 115 private Message mSource; 116 117 // we use mAction instead of Intent.getAction() because sometimes we need to 118 // re-write the action to EDIT_DRAFT. 119 private String mAction; 120 121 /** 122 * Indicates that the source message has been processed at least once and should not 123 * be processed on any subsequent loads. This protects us from adding attachments that 124 * have already been added from the restore of the view state. 125 */ 126 private boolean mSourceMessageProcessed = false; 127 128 private MultiAutoCompleteTextView mToView; 129 private MultiAutoCompleteTextView mCcView; 130 private MultiAutoCompleteTextView mBccView; 131 private EditText mSubjectView; 132 private EditText mMessageContentView; 133 private Button mSendButton; 134 private Button mDiscardButton; 135 private Button mSaveButton; 136 private LinearLayout mAttachments; 137 private View mQuotedTextBar; 138 private ImageButton mQuotedTextDelete; 139 private WebView mQuotedText; 140 private TextView mLeftTitle; 141 private TextView mRightTitle; 142 143 private Controller mController; 144 private Listener mListener = new Listener(); 145 private boolean mDraftNeedsSaving; 146 private boolean mMessageLoaded; 147 private AsyncTask mLoadAttachmentsTask; 148 private AsyncTask mSaveMessageTask; 149 private AsyncTask mLoadMessageTask; 150 151 private EmailAddressAdapter mAddressAdapter; 152 153 private Handler mHandler = new Handler() { 154 @Override 155 public void handleMessage(android.os.Message msg) { 156 switch (msg.what) { 157 case MSG_UPDATE_TITLE: 158 updateTitle(); 159 break; 160 case MSG_SKIPPED_ATTACHMENTS: 161 Toast.makeText( 162 MessageCompose.this, 163 getString(R.string.message_compose_attachments_skipped_toast), 164 Toast.LENGTH_LONG).show(); 165 break; 166 default: 167 super.handleMessage(msg); 168 break; 169 } 170 } 171 }; 172 173 /** 174 * Compose a new message using the given account. If account is -1 the default account 175 * will be used. 176 * @param context 177 * @param accountId 178 */ 179 public static void actionCompose(Context context, long accountId) { 180 try { 181 Intent i = new Intent(context, MessageCompose.class); 182 i.putExtra(EXTRA_ACCOUNT_ID, accountId); 183 context.startActivity(i); 184 } catch (ActivityNotFoundException anfe) { 185 // Swallow it - this is usually a race condition, especially under automated test. 186 // (The message composer might have been disabled) 187 Email.log(anfe.toString()); 188 } 189 } 190 191 /** 192 * Compose a new message using a uri (mailto:) and a given account. If account is -1 the 193 * default account will be used. 194 * @param context 195 * @param uriString 196 * @param accountId 197 * @return true if startActivity() succeeded 198 */ 199 public static boolean actionCompose(Context context, String uriString, long accountId) { 200 try { 201 Intent i = new Intent(context, MessageCompose.class); 202 i.setAction(Intent.ACTION_SEND); 203 i.setData(Uri.parse(uriString)); 204 i.putExtra(EXTRA_ACCOUNT_ID, accountId); 205 context.startActivity(i); 206 return true; 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 Email.log(anfe.toString()); 211 return false; 212 } 213 } 214 215 /** 216 * Compose a new message as a reply to the given message. If replyAll is true the function 217 * is reply all instead of simply reply. 218 * @param context 219 * @param messageId 220 * @param replyAll 221 */ 222 public static void actionReply(Context context, long messageId, boolean replyAll) { 223 startActivityWithMessage(context, replyAll ? ACTION_REPLY_ALL : ACTION_REPLY, messageId); 224 } 225 226 /** 227 * Compose a new message as a forward of the given message. 228 * @param context 229 * @param messageId 230 */ 231 public static void actionForward(Context context, long messageId) { 232 startActivityWithMessage(context, ACTION_FORWARD, messageId); 233 } 234 235 /** 236 * Continue composition of the given message. This action modifies the way this Activity 237 * handles certain actions. 238 * Save will attempt to replace the message in the given folder with the updated version. 239 * Discard will delete the message from the given folder. 240 * @param context 241 * @param messageId the message id. 242 */ 243 public static void actionEditDraft(Context context, long messageId) { 244 startActivityWithMessage(context, ACTION_EDIT_DRAFT, messageId); 245 } 246 247 private static void startActivityWithMessage(Context context, String action, long messageId) { 248 Intent i = new Intent(context, MessageCompose.class); 249 i.putExtra(EXTRA_MESSAGE_ID, messageId); 250 i.setAction(action); 251 context.startActivity(i); 252 } 253 254 private void setAccount(Intent intent) { 255 long accountId = intent.getLongExtra(EXTRA_ACCOUNT_ID, -1); 256 if (accountId == -1) { 257 accountId = Account.getDefaultAccountId(this); 258 } 259 if (accountId == -1) { 260 // There are no accounts set up. This should not have happened. Prompt the 261 // user to set up an account as an acceptable bailout. 262 AccountFolderList.actionShowAccounts(this); 263 finish(); 264 } else { 265 setAccount(Account.restoreAccountWithId(this, accountId)); 266 } 267 } 268 269 private void setAccount(Account account) { 270 mAccount = account; 271 if (account != null) { 272 mRightTitle.setText(account.mDisplayName); 273 } 274 } 275 276 @Override 277 public void onCreate(Bundle savedInstanceState) { 278 super.onCreate(savedInstanceState); 279 requestWindowFeature(Window.FEATURE_CUSTOM_TITLE); 280 setContentView(R.layout.message_compose); 281 getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE, R.layout.list_title); 282 283 mController = Controller.getInstance(getApplication()); 284 initViews(); 285 setDraftNeedsSaving(false); 286 287 long draftId = -1; 288 if (savedInstanceState != null) { 289 // This data gets used in onCreate, so grab it here instead of onRestoreInstanceState 290 mSourceMessageProcessed = 291 savedInstanceState.getBoolean(STATE_KEY_SOURCE_MESSAGE_PROCED, false); 292 draftId = savedInstanceState.getLong(STATE_KEY_DRAFT_ID, -1); 293 } 294 295 Intent intent = getIntent(); 296 mAction = intent.getAction(); 297 298 if (draftId != -1) { 299 // this means that we saved the draft earlier, 300 // so now we need to disregard the intent action and do 301 // EDIT_DRAFT instead. 302 mAction = ACTION_EDIT_DRAFT; 303 mDraft.mId = draftId; 304 } 305 306 // Handle the various intents that launch the message composer 307 if (Intent.ACTION_VIEW.equals(mAction) 308 || Intent.ACTION_SENDTO.equals(mAction) 309 || Intent.ACTION_SEND.equals(mAction) 310 || Intent.ACTION_SEND_MULTIPLE.equals(mAction)) { 311 setAccount(intent); 312 // Use the fields found in the Intent to prefill as much of the message as possible 313 initFromIntent(intent); 314 setDraftNeedsSaving(true); 315 mMessageLoaded = true; 316 mSourceMessageProcessed = true; 317 } else { 318 // Otherwise, handle the internal cases (Message Composer invoked from within app) 319 long messageId = draftId != -1 ? draftId : intent.getLongExtra(EXTRA_MESSAGE_ID, -1); 320 if (messageId != -1) { 321 mLoadMessageTask = new LoadMessageTask().execute(messageId); 322 } else { 323 setAccount(intent); 324 // Since this is a new message, we don't need to call LoadMessageTask. 325 // But we DO need to set mMessageLoaded to indicate the message can be sent 326 mMessageLoaded = true; 327 mSourceMessageProcessed = true; 328 } 329 } 330 331 if (ACTION_REPLY.equals(mAction) || ACTION_REPLY_ALL.equals(mAction) || 332 ACTION_FORWARD.equals(mAction) || ACTION_EDIT_DRAFT.equals(mAction)) { 333 /* 334 * If we need to load the message we add ourself as a message listener here 335 * so we can kick it off. Normally we add in onResume but we don't 336 * want to reload the message every time the activity is resumed. 337 * There is no harm in adding twice. 338 */ 339 // TODO: signal the controller to load the message 340 } 341 updateTitle(); 342 } 343 344 // needed for unit tests 345 @Override 346 public void setIntent(Intent intent) { 347 super.setIntent(intent); 348 mAction = intent.getAction(); 349 } 350 351 @Override 352 public void onResume() { 353 super.onResume(); 354 mController.addResultCallback(mListener); 355 } 356 357 @Override 358 public void onPause() { 359 super.onPause(); 360 saveIfNeeded(); 361 mController.removeResultCallback(mListener); 362 } 363 364 private static void cancelTask(AsyncTask<?, ?, ?> task) { 365 if (task != null && task.getStatus() != AsyncTask.Status.FINISHED) { 366 task.cancel(true); 367 } 368 } 369 370 /** 371 * We override onDestroy to make sure that the WebView gets explicitly destroyed. 372 * Otherwise it can leak native references. 373 */ 374 @Override 375 public void onDestroy() { 376 super.onDestroy(); 377 mQuotedText.destroy(); 378 mQuotedText = null; 379 cancelTask(mLoadAttachmentsTask); 380 mLoadAttachmentsTask = null; 381 cancelTask(mLoadMessageTask); 382 mLoadMessageTask = null; 383 // don't cancel mSaveMessageTask, let it do its job to the end. 384 385 // Make sure the adapter doesn't leak its cursor 386 if (mAddressAdapter != null) { 387 mAddressAdapter.changeCursor(null); 388 } 389 } 390 391 /** 392 * The framework handles most of the fields, but we need to handle stuff that we 393 * dynamically show and hide: 394 * Cc field, 395 * Bcc field, 396 * Quoted text, 397 */ 398 @Override 399 protected void onSaveInstanceState(Bundle outState) { 400 super.onSaveInstanceState(outState); 401 long draftId = getOrCreateDraftId(); 402 if (draftId != -1) { 403 outState.putLong(STATE_KEY_DRAFT_ID, draftId); 404 } 405 outState.putBoolean(STATE_KEY_CC_SHOWN, mCcView.getVisibility() == View.VISIBLE); 406 outState.putBoolean(STATE_KEY_BCC_SHOWN, mBccView.getVisibility() == View.VISIBLE); 407 outState.putBoolean(STATE_KEY_QUOTED_TEXT_SHOWN, 408 mQuotedTextBar.getVisibility() == View.VISIBLE); 409 outState.putBoolean(STATE_KEY_SOURCE_MESSAGE_PROCED, mSourceMessageProcessed); 410 } 411 412 @Override 413 protected void onRestoreInstanceState(Bundle savedInstanceState) { 414 super.onRestoreInstanceState(savedInstanceState); 415 mCcView.setVisibility(savedInstanceState.getBoolean(STATE_KEY_CC_SHOWN) ? 416 View.VISIBLE : View.GONE); 417 mBccView.setVisibility(savedInstanceState.getBoolean(STATE_KEY_BCC_SHOWN) ? 418 View.VISIBLE : View.GONE); 419 mQuotedTextBar.setVisibility(savedInstanceState.getBoolean(STATE_KEY_QUOTED_TEXT_SHOWN) ? 420 View.VISIBLE : View.GONE); 421 mQuotedText.setVisibility(savedInstanceState.getBoolean(STATE_KEY_QUOTED_TEXT_SHOWN) ? 422 View.VISIBLE : View.GONE); 423 setDraftNeedsSaving(false); 424 } 425 426 private void setDraftNeedsSaving(boolean needsSaving) { 427 mDraftNeedsSaving = needsSaving; 428 mSaveButton.setEnabled(needsSaving); 429 } 430 431 private void initViews() { 432 mToView = (MultiAutoCompleteTextView)findViewById(R.id.to); 433 mCcView = (MultiAutoCompleteTextView)findViewById(R.id.cc); 434 mBccView = (MultiAutoCompleteTextView)findViewById(R.id.bcc); 435 mSubjectView = (EditText)findViewById(R.id.subject); 436 mMessageContentView = (EditText)findViewById(R.id.message_content); 437 mSendButton = (Button)findViewById(R.id.send); 438 mDiscardButton = (Button)findViewById(R.id.discard); 439 mSaveButton = (Button)findViewById(R.id.save); 440 mAttachments = (LinearLayout)findViewById(R.id.attachments); 441 mQuotedTextBar = findViewById(R.id.quoted_text_bar); 442 mQuotedTextDelete = (ImageButton)findViewById(R.id.quoted_text_delete); 443 mQuotedText = (WebView)findViewById(R.id.quoted_text); 444 mLeftTitle = (TextView)findViewById(R.id.title_left_text); 445 mRightTitle = (TextView)findViewById(R.id.title_right_text); 446 447 TextWatcher watcher = new TextWatcher() { 448 public void beforeTextChanged(CharSequence s, int start, 449 int before, int after) { } 450 451 public void onTextChanged(CharSequence s, int start, 452 int before, int count) { 453 setDraftNeedsSaving(true); 454 } 455 456 public void afterTextChanged(android.text.Editable s) { } 457 }; 458 459 /** 460 * Implements special address cleanup rules: 461 * The first space key entry following an "@" symbol that is followed by any combination 462 * of letters and symbols, including one+ dots and zero commas, should insert an extra 463 * comma (followed by the space). 464 */ 465 InputFilter recipientFilter = new InputFilter() { 466 467 public CharSequence filter(CharSequence source, int start, int end, Spanned dest, 468 int dstart, int dend) { 469 470 // quick check - did they enter a single space? 471 if (end-start != 1 || source.charAt(start) != ' ') { 472 return null; 473 } 474 475 // determine if the characters before the new space fit the pattern 476 // follow backwards and see if we find a comma, dot, or @ 477 int scanBack = dstart; 478 boolean dotFound = false; 479 while (scanBack > 0) { 480 char c = dest.charAt(--scanBack); 481 switch (c) { 482 case '.': 483 dotFound = true; // one or more dots are req'd 484 break; 485 case ',': 486 return null; 487 case '@': 488 if (!dotFound) { 489 return null; 490 } 491 492 // we have found a comma-insert case. now just do it 493 // in the least expensive way we can. 494 if (source instanceof Spanned) { 495 SpannableStringBuilder sb = new SpannableStringBuilder(","); 496 sb.append(source); 497 return sb; 498 } else { 499 return ", "; 500 } 501 default: 502 // just keep going 503 } 504 } 505 506 // no termination cases were found, so don't edit the input 507 return null; 508 } 509 }; 510 InputFilter[] recipientFilters = new InputFilter[] { recipientFilter }; 511 512 mToView.addTextChangedListener(watcher); 513 mCcView.addTextChangedListener(watcher); 514 mBccView.addTextChangedListener(watcher); 515 mSubjectView.addTextChangedListener(watcher); 516 mMessageContentView.addTextChangedListener(watcher); 517 518 // NOTE: assumes no other filters are set 519 mToView.setFilters(recipientFilters); 520 mCcView.setFilters(recipientFilters); 521 mBccView.setFilters(recipientFilters); 522 523 /* 524 * We set this to invisible by default. Other methods will turn it back on if it's 525 * needed. 526 */ 527 mQuotedTextBar.setVisibility(View.GONE); 528 mQuotedText.setVisibility(View.GONE); 529 530 mQuotedTextDelete.setOnClickListener(this); 531 532 mAddressAdapter = new EmailAddressAdapter(this); 533 EmailAddressValidator addressValidator = new EmailAddressValidator(); 534 535 mToView.setAdapter(mAddressAdapter); 536 mToView.setTokenizer(new Rfc822Tokenizer()); 537 mToView.setValidator(addressValidator); 538 539 mCcView.setAdapter(mAddressAdapter); 540 mCcView.setTokenizer(new Rfc822Tokenizer()); 541 mCcView.setValidator(addressValidator); 542 543 mBccView.setAdapter(mAddressAdapter); 544 mBccView.setTokenizer(new Rfc822Tokenizer()); 545 mBccView.setValidator(addressValidator); 546 547 mSendButton.setOnClickListener(this); 548 mDiscardButton.setOnClickListener(this); 549 mSaveButton.setOnClickListener(this); 550 551 mSubjectView.setOnFocusChangeListener(this); 552 } 553 554 // TODO: is there any way to unify this with MessageView.LoadMessageTask? 555 private class LoadMessageTask extends AsyncTask<Long, Void, Object[]> { 556 @Override 557 protected Object[] doInBackground(Long... messageIds) { 558 Message message = Message.restoreMessageWithId(MessageCompose.this, messageIds[0]); 559 if (message == null) { 560 return new Object[] {null, null}; 561 } 562 long accountId = message.mAccountKey; 563 Account account = Account.restoreAccountWithId(MessageCompose.this, accountId); 564 try { 565 // Body body = Body.restoreBodyWithMessageId(MessageCompose.this, message.mId); 566 message.mHtml = Body.restoreBodyHtmlWithMessageId(MessageCompose.this, message.mId); 567 message.mText = Body.restoreBodyTextWithMessageId(MessageCompose.this, message.mId); 568 boolean isEditDraft = ACTION_EDIT_DRAFT.equals(mAction); 569 // the reply fields are only filled/used for Drafts. 570 if (isEditDraft) { 571 message.mHtmlReply = 572 Body.restoreReplyHtmlWithMessageId(MessageCompose.this, message.mId); 573 message.mTextReply = 574 Body.restoreReplyTextWithMessageId(MessageCompose.this, message.mId); 575 message.mIntroText = 576 Body.restoreIntroTextWithMessageId(MessageCompose.this, message.mId); 577 } else { 578 message.mHtmlReply = null; 579 message.mTextReply = null; 580 message.mIntroText = null; 581 } 582 } catch (RuntimeException e) { 583 Log.d(Email.LOG_TAG, "Exception while loading message body: " + e); 584 return new Object[] {null, null}; 585 } 586 return new Object[]{message, account}; 587 } 588 589 @Override 590 protected void onPostExecute(Object[] messageAndAccount) { 591 if (messageAndAccount == null) { 592 return; 593 } 594 595 final Message message = (Message) messageAndAccount[0]; 596 final Account account = (Account) messageAndAccount[1]; 597 if (message == null && account == null) { 598 // Something unexpected happened: 599 // the message or the body couldn't be loaded by SQLite. 600 // Bail out. 601 Toast.makeText(MessageCompose.this, R.string.error_loading_message_body, 602 Toast.LENGTH_LONG).show(); 603 finish(); 604 return; 605 } 606 607 if (ACTION_EDIT_DRAFT.equals(mAction)) { 608 mDraft = message; 609 mLoadAttachmentsTask = new AsyncTask<Long, Void, Attachment[]>() { 610 @Override 611 protected Attachment[] doInBackground(Long... messageIds) { 612 return Attachment.restoreAttachmentsWithMessageId(MessageCompose.this, 613 messageIds[0]); 614 } 615 @Override 616 protected void onPostExecute(Attachment[] attachments) { 617 if (attachments == null) { 618 return; 619 } 620 for (Attachment attachment : attachments) { 621 addAttachment(attachment); 622 } 623 } 624 }.execute(message.mId); 625 } else if (ACTION_REPLY.equals(mAction) 626 || ACTION_REPLY_ALL.equals(mAction) 627 || ACTION_FORWARD.equals(mAction)) { 628 mSource = message; 629 } else if (Email.LOGD) { 630 Email.log("Action " + mAction + " has unexpected EXTRA_MESSAGE_ID"); 631 } 632 633 setAccount(account); 634 processSourceMessageGuarded(message, mAccount); 635 mMessageLoaded = true; 636 } 637 } 638 639 private void updateTitle() { 640 if (mSubjectView.getText().length() == 0) { 641 mLeftTitle.setText(R.string.compose_title); 642 } else { 643 mLeftTitle.setText(mSubjectView.getText().toString()); 644 } 645 } 646 647 public void onFocusChange(View view, boolean focused) { 648 if (!focused) { 649 updateTitle(); 650 } 651 } 652 653 private void addAddresses(MultiAutoCompleteTextView view, Address[] addresses) { 654 if (addresses == null) { 655 return; 656 } 657 for (Address address : addresses) { 658 addAddress(view, address.toString()); 659 } 660 } 661 662 private void addAddresses(MultiAutoCompleteTextView view, String[] addresses) { 663 if (addresses == null) { 664 return; 665 } 666 for (String oneAddress : addresses) { 667 addAddress(view, oneAddress); 668 } 669 } 670 671 private void addAddress(MultiAutoCompleteTextView view, String address) { 672 view.append(address + ", "); 673 } 674 675 private String getPackedAddresses(TextView view) { 676 Address[] addresses = Address.parse(view.getText().toString().trim()); 677 return Address.pack(addresses); 678 } 679 680 private Address[] getAddresses(TextView view) { 681 Address[] addresses = Address.parse(view.getText().toString().trim()); 682 return addresses; 683 } 684 685 /* 686 * Computes a short string indicating the destination of the message based on To, Cc, Bcc. 687 * If only one address appears, returns the friendly form of that address. 688 * Otherwise returns the friendly form of the first address appended with "and N others". 689 */ 690 private String makeDisplayName(String packedTo, String packedCc, String packedBcc) { 691 Address first = null; 692 int nRecipients = 0; 693 for (String packed: new String[] {packedTo, packedCc, packedBcc}) { 694 Address[] addresses = Address.unpack(packed); 695 nRecipients += addresses.length; 696 if (first == null && addresses.length > 0) { 697 first = addresses[0]; 698 } 699 } 700 if (nRecipients == 0) { 701 return ""; 702 } 703 String friendly = first.toFriendly(); 704 if (nRecipients == 1) { 705 return friendly; 706 } 707 return this.getString(R.string.message_compose_display_name, friendly, nRecipients - 1); 708 } 709 710 private ContentValues getUpdateContentValues(Message message) { 711 ContentValues values = new ContentValues(); 712 values.put(MessageColumns.TIMESTAMP, message.mTimeStamp); 713 values.put(MessageColumns.FROM_LIST, message.mFrom); 714 values.put(MessageColumns.TO_LIST, message.mTo); 715 values.put(MessageColumns.CC_LIST, message.mCc); 716 values.put(MessageColumns.BCC_LIST, message.mBcc); 717 values.put(MessageColumns.SUBJECT, message.mSubject); 718 values.put(MessageColumns.DISPLAY_NAME, message.mDisplayName); 719 values.put(MessageColumns.FLAG_READ, message.mFlagRead); 720 values.put(MessageColumns.FLAG_LOADED, message.mFlagLoaded); 721 values.put(MessageColumns.FLAG_ATTACHMENT, message.mFlagAttachment); 722 values.put(MessageColumns.FLAGS, message.mFlags); 723 return values; 724 } 725 726 /** 727 * @param message The message to be updated. 728 * @param account the account (used to obtain From: address). 729 * @param bodyText the body text. 730 */ 731 private void updateMessage(Message message, Account account, boolean hasAttachments) { 732 if (message.mMessageId == null || message.mMessageId.length() == 0) { 733 message.mMessageId = Utility.generateMessageId(); 734 } 735 message.mTimeStamp = System.currentTimeMillis(); 736 message.mFrom = new Address(account.getEmailAddress(), account.getSenderName()).pack(); 737 message.mTo = getPackedAddresses(mToView); 738 message.mCc = getPackedAddresses(mCcView); 739 message.mBcc = getPackedAddresses(mBccView); 740 message.mSubject = mSubjectView.getText().toString(); 741 message.mText = mMessageContentView.getText().toString(); 742 message.mAccountKey = account.mId; 743 message.mDisplayName = makeDisplayName(message.mTo, message.mCc, message.mBcc); 744 message.mFlagRead = true; 745 message.mFlagLoaded = Message.FLAG_LOADED_COMPLETE; 746 message.mFlagAttachment = hasAttachments; 747 // Use the Intent to set flags saying this message is a reply or a forward and save the 748 // unique id of the source message 749 if (mSource != null && mQuotedTextBar.getVisibility() == View.VISIBLE) { 750 if (ACTION_REPLY.equals(mAction) || ACTION_REPLY_ALL.equals(mAction) 751 || ACTION_FORWARD.equals(mAction)) { 752 message.mSourceKey = mSource.mId; 753 // Get the body of the source message here 754 // Note that the following commented line will be useful when we use HTML in replies 755 //message.mHtmlReply = mSource.mHtml; 756 message.mTextReply = mSource.mText; 757 } 758 759 String fromAsString = Address.unpackToString(mSource.mFrom); 760 if (ACTION_FORWARD.equals(mAction)) { 761 message.mFlags |= Message.FLAG_TYPE_FORWARD; 762 String subject = mSource.mSubject; 763 String to = Address.unpackToString(mSource.mTo); 764 String cc = Address.unpackToString(mSource.mCc); 765 message.mIntroText = 766 getString(R.string.message_compose_fwd_header_fmt, subject, fromAsString, 767 to != null ? to : "", cc != null ? cc : ""); 768 } else { 769 message.mFlags |= Message.FLAG_TYPE_REPLY; 770 message.mIntroText = 771 getString(R.string.message_compose_reply_header_fmt, fromAsString); 772 } 773 } 774 } 775 776 private Attachment[] getAttachmentsFromUI() { 777 int count = mAttachments.getChildCount(); 778 Attachment[] attachments = new Attachment[count]; 779 for (int i = 0; i < count; ++i) { 780 attachments[i] = (Attachment) mAttachments.getChildAt(i).getTag(); 781 } 782 return attachments; 783 } 784 785 /* This method does DB operations in UI thread because 786 the draftId is needed by onSaveInstanceState() which can't wait for it 787 to be saved in the background. 788 TODO: This will cause ANRs, so we need to find a better solution. 789 */ 790 private long getOrCreateDraftId() { 791 synchronized (mDraft) { 792 if (mDraft.mId > 0) { 793 return mDraft.mId; 794 } 795 // don't save draft if the source message did not load yet 796 if (!mMessageLoaded) { 797 return -1; 798 } 799 final Attachment[] attachments = getAttachmentsFromUI(); 800 updateMessage(mDraft, mAccount, attachments.length > 0); 801 mController.saveToMailbox(mDraft, EmailContent.Mailbox.TYPE_DRAFTS); 802 return mDraft.mId; 803 } 804 } 805 806 /** 807 * Send or save a message: 808 * - out of the UI thread 809 * - write to Drafts 810 * - if send, invoke Controller.sendMessage() 811 * - when operation is complete, display toast 812 */ 813 private void sendOrSaveMessage(final boolean send) { 814 final Attachment[] attachments = getAttachmentsFromUI(); 815 if (!mMessageLoaded) { 816 // early save, before the message was loaded: do nothing 817 return; 818 } 819 updateMessage(mDraft, mAccount, attachments.length > 0); 820 821 mSaveMessageTask = new AsyncTask<Void, Void, Void>() { 822 @Override 823 protected Void doInBackground(Void... params) { 824 synchronized (mDraft) { 825 if (mDraft.isSaved()) { 826 // Update the message 827 Uri draftUri = 828 ContentUris.withAppendedId(mDraft.SYNCED_CONTENT_URI, mDraft.mId); 829 getContentResolver().update(draftUri, getUpdateContentValues(mDraft), 830 null, null); 831 // Update the body 832 ContentValues values = new ContentValues(); 833 values.put(BodyColumns.TEXT_CONTENT, mDraft.mText); 834 values.put(BodyColumns.TEXT_REPLY, mDraft.mTextReply); 835 values.put(BodyColumns.HTML_REPLY, mDraft.mHtmlReply); 836 values.put(BodyColumns.INTRO_TEXT, mDraft.mIntroText); 837 Body.updateBodyWithMessageId(MessageCompose.this, mDraft.mId, values); 838 } else { 839 // mDraft.mId is set upon return of saveToMailbox() 840 mController.saveToMailbox(mDraft, EmailContent.Mailbox.TYPE_DRAFTS); 841 } 842 for (Attachment attachment : attachments) { 843 if (!attachment.isSaved()) { 844 // this attachment is new so save it to DB. 845 attachment.mMessageKey = mDraft.mId; 846 attachment.save(MessageCompose.this); 847 } 848 } 849 850 if (send) { 851 mController.sendMessage(mDraft.mId, mDraft.mAccountKey); 852 } 853 return null; 854 } 855 } 856 857 @Override 858 protected void onPostExecute(Void dummy) { 859 if (isCancelled()) { 860 return; 861 } 862 // Don't display the toast if the user is just changing the orientation 863 if (!send && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) { 864 Toast.makeText(MessageCompose.this, R.string.message_saved_toast, 865 Toast.LENGTH_LONG).show(); 866 } 867 } 868 }.execute(); 869 } 870 871 private void saveIfNeeded() { 872 if (!mDraftNeedsSaving) { 873 return; 874 } 875 setDraftNeedsSaving(false); 876 sendOrSaveMessage(false); 877 } 878 879 /** 880 * Checks whether all the email addresses listed in TO, CC, BCC are valid. 881 */ 882 /* package */ boolean isAddressAllValid() { 883 for (TextView view : new TextView[]{mToView, mCcView, mBccView}) { 884 String addresses = view.getText().toString().trim(); 885 if (!Address.isAllValid(addresses)) { 886 view.setError(getString(R.string.message_compose_error_invalid_email)); 887 return false; 888 } 889 } 890 return true; 891 } 892 893 private void onSend() { 894 if (!isAddressAllValid()) { 895 Toast.makeText(this, getString(R.string.message_compose_error_invalid_email), 896 Toast.LENGTH_LONG).show(); 897 } else if (getAddresses(mToView).length == 0 && 898 getAddresses(mCcView).length == 0 && 899 getAddresses(mBccView).length == 0) { 900 mToView.setError(getString(R.string.message_compose_error_no_recipients)); 901 Toast.makeText(this, getString(R.string.message_compose_error_no_recipients), 902 Toast.LENGTH_LONG).show(); 903 } else { 904 sendOrSaveMessage(true); 905 setDraftNeedsSaving(false); 906 finish(); 907 } 908 } 909 910 private void onDiscard() { 911 if (mDraft.mId > 0) { 912 mController.deleteMessage(mDraft.mId, mDraft.mAccountKey); 913 } 914 Toast.makeText(this, getString(R.string.message_discarded_toast), Toast.LENGTH_LONG).show(); 915 setDraftNeedsSaving(false); 916 finish(); 917 } 918 919 private void onSave() { 920 saveIfNeeded(); 921 finish(); 922 } 923 924 private void onAddCcBcc() { 925 mCcView.setVisibility(View.VISIBLE); 926 mBccView.setVisibility(View.VISIBLE); 927 } 928 929 /** 930 * Kick off a picker for whatever kind of MIME types we'll accept and let Android take over. 931 */ 932 private void onAddAttachment() { 933 Intent i = new Intent(Intent.ACTION_GET_CONTENT); 934 i.addCategory(Intent.CATEGORY_OPENABLE); 935 i.setType(Email.ACCEPTABLE_ATTACHMENT_SEND_UI_TYPES[0]); 936 startActivityForResult( 937 Intent.createChooser(i, getString(R.string.choose_attachment_dialog_title)), 938 ACTIVITY_REQUEST_PICK_ATTACHMENT); 939 } 940 941 private Attachment loadAttachmentInfo(Uri uri) { 942 int size = -1; 943 String name = null; 944 ContentResolver contentResolver = getContentResolver(); 945 Cursor metadataCursor = contentResolver.query(uri, 946 ATTACHMENT_META_COLUMNS, null, null, null); 947 if (metadataCursor != null) { 948 try { 949 if (metadataCursor.moveToFirst()) { 950 name = metadataCursor.getString(0); 951 size = metadataCursor.getInt(1); 952 } 953 } finally { 954 metadataCursor.close(); 955 } 956 } 957 if (name == null) { 958 name = uri.getLastPathSegment(); 959 } 960 961 String contentType = contentResolver.getType(uri); 962 if (contentType == null) { 963 contentType = ""; 964 } 965 966 Attachment attachment = new Attachment(); 967 attachment.mFileName = name; 968 attachment.mContentUri = uri.toString(); 969 attachment.mSize = size; 970 attachment.mMimeType = contentType; 971 return attachment; 972 } 973 974 private void addAttachment(Attachment attachment) { 975 // Before attaching the attachment, make sure it meets any other pre-attach criteria 976 if (attachment.mSize > Email.MAX_ATTACHMENT_UPLOAD_SIZE) { 977 Toast.makeText(this, R.string.message_compose_attachment_size, Toast.LENGTH_LONG) 978 .show(); 979 return; 980 } 981 982 View view = getLayoutInflater().inflate(R.layout.message_compose_attachment, 983 mAttachments, false); 984 TextView nameView = (TextView)view.findViewById(R.id.attachment_name); 985 ImageButton delete = (ImageButton)view.findViewById(R.id.attachment_delete); 986 nameView.setText(attachment.mFileName); 987 delete.setOnClickListener(this); 988 delete.setTag(view); 989 view.setTag(attachment); 990 mAttachments.addView(view); 991 } 992 993 private void addAttachment(Uri uri) { 994 addAttachment(loadAttachmentInfo(uri)); 995 } 996 997 @Override 998 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 999 if (data == null) { 1000 return; 1001 } 1002 addAttachment(data.getData()); 1003 setDraftNeedsSaving(true); 1004 } 1005 1006 public void onClick(View view) { 1007 switch (view.getId()) { 1008 case R.id.send: 1009 onSend(); 1010 break; 1011 case R.id.save: 1012 onSave(); 1013 break; 1014 case R.id.discard: 1015 onDiscard(); 1016 break; 1017 case R.id.attachment_delete: 1018 onDeleteAttachment(view); 1019 break; 1020 case R.id.quoted_text_delete: 1021 mQuotedTextBar.setVisibility(View.GONE); 1022 mQuotedText.setVisibility(View.GONE); 1023 mDraft.mIntroText = null; 1024 mDraft.mTextReply = null; 1025 mDraft.mHtmlReply = null; 1026 setDraftNeedsSaving(true); 1027 break; 1028 } 1029 } 1030 1031 private void onDeleteAttachment(View delButtonView) { 1032 /* 1033 * The view is the delete button, and we have previously set the tag of 1034 * the delete button to the view that owns it. We don't use parent because the 1035 * view is very complex and could change in the future. 1036 */ 1037 View attachmentView = (View) delButtonView.getTag(); 1038 Attachment attachment = (Attachment) attachmentView.getTag(); 1039 mAttachments.removeView(attachmentView); 1040 if (attachment.isSaved()) { 1041 // The following async task for deleting attachments: 1042 // - can be started multiple times in parallel (to delete multiple attachments). 1043 // - need not be interrupted on activity exit, instead should run to completion. 1044 new AsyncTask<Long, Void, Void>() { 1045 @Override 1046 protected Void doInBackground(Long... attachmentIds) { 1047 mController.deleteAttachment(attachmentIds[0]); 1048 return null; 1049 } 1050 }.execute(attachment.mId); 1051 } 1052 setDraftNeedsSaving(true); 1053 } 1054 1055 @Override 1056 public boolean onOptionsItemSelected(MenuItem item) { 1057 switch (item.getItemId()) { 1058 case R.id.send: 1059 onSend(); 1060 break; 1061 case R.id.save: 1062 onSave(); 1063 break; 1064 case R.id.discard: 1065 onDiscard(); 1066 break; 1067 case R.id.add_cc_bcc: 1068 onAddCcBcc(); 1069 break; 1070 case R.id.add_attachment: 1071 onAddAttachment(); 1072 break; 1073 default: 1074 return super.onOptionsItemSelected(item); 1075 } 1076 return true; 1077 } 1078 1079 @Override 1080 public boolean onCreateOptionsMenu(Menu menu) { 1081 super.onCreateOptionsMenu(menu); 1082 getMenuInflater().inflate(R.menu.message_compose_option, menu); 1083 return true; 1084 } 1085 1086 /** 1087 * Returns true if all attachments were able to be attached, otherwise returns false. 1088 */ 1089// private boolean loadAttachments(Part part, int depth) throws MessagingException { 1090// if (part.getBody() instanceof Multipart) { 1091// Multipart mp = (Multipart) part.getBody(); 1092// boolean ret = true; 1093// for (int i = 0, count = mp.getCount(); i < count; i++) { 1094// if (!loadAttachments(mp.getBodyPart(i), depth + 1)) { 1095// ret = false; 1096// } 1097// } 1098// return ret; 1099// } else { 1100// String contentType = MimeUtility.unfoldAndDecode(part.getContentType()); 1101// String name = MimeUtility.getHeaderParameter(contentType, "name"); 1102// if (name != null) { 1103// Body body = part.getBody(); 1104// if (body != null && body instanceof LocalAttachmentBody) { 1105// final Uri uri = ((LocalAttachmentBody) body).getContentUri(); 1106// mHandler.post(new Runnable() { 1107// public void run() { 1108// addAttachment(uri); 1109// } 1110// }); 1111// } 1112// else { 1113// return false; 1114// } 1115// } 1116// return true; 1117// } 1118// } 1119 1120 /** 1121 * Fill all the widgets with the content found in the Intent Extra, if any. 1122 * 1123 * Note that we don't actually check the intent action (typically VIEW, SENDTO, or SEND). 1124 * There is enough overlap in the definitions that it makes more sense to simply check for 1125 * all available data and use as much of it as possible. 1126 * 1127 * With one exception: EXTRA_STREAM is defined as only valid for ACTION_SEND. 1128 * 1129 * @param intent the launch intent 1130 */ 1131 /* package */ void initFromIntent(Intent intent) { 1132 1133 // First, add values stored in top-level extras 1134 1135 String[] extraStrings = intent.getStringArrayExtra(Intent.EXTRA_EMAIL); 1136 if (extraStrings != null) { 1137 addAddresses(mToView, extraStrings); 1138 } 1139 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_CC); 1140 if (extraStrings != null) { 1141 addAddresses(mCcView, extraStrings); 1142 } 1143 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_BCC); 1144 if (extraStrings != null) { 1145 addAddresses(mBccView, extraStrings); 1146 } 1147 String extraString = intent.getStringExtra(Intent.EXTRA_SUBJECT); 1148 if (extraString != null) { 1149 mSubjectView.setText(extraString); 1150 } 1151 1152 // Next, if we were invoked with a URI, try to interpret it 1153 // We'll take two courses here. If it's mailto:, there is a specific set of rules 1154 // that define various optional fields. However, for any other scheme, we'll simply 1155 // take the entire scheme-specific part and interpret it as a possible list of addresses. 1156 1157 final Uri dataUri = intent.getData(); 1158 if (dataUri != null) { 1159 if ("mailto".equals(dataUri.getScheme())) { 1160 initializeFromMailTo(dataUri.toString()); 1161 } else { 1162 String toText = dataUri.getSchemeSpecificPart(); 1163 if (toText != null) { 1164 addAddresses(mToView, toText.split(",")); 1165 } 1166 } 1167 } 1168 1169 // Next, fill in the plaintext (note, this will override mailto:?body=) 1170 1171 CharSequence text = intent.getCharSequenceExtra(Intent.EXTRA_TEXT); 1172 if (text != null) { 1173 mMessageContentView.setText(text); 1174 } 1175 1176 // Next, convert EXTRA_STREAM into an attachment 1177 1178 if (Intent.ACTION_SEND.equals(mAction) && intent.hasExtra(Intent.EXTRA_STREAM)) { 1179 String type = intent.getType(); 1180 Uri stream = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM); 1181 if (stream != null && type != null) { 1182 if (MimeUtility.mimeTypeMatches(type, 1183 Email.ACCEPTABLE_ATTACHMENT_SEND_INTENT_TYPES)) { 1184 addAttachment(stream); 1185 } 1186 } 1187 } 1188 1189 if (Intent.ACTION_SEND_MULTIPLE.equals(mAction) 1190 && intent.hasExtra(Intent.EXTRA_STREAM)) { 1191 ArrayList<Parcelable> list = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); 1192 if (list != null) { 1193 for (Parcelable parcelable : list) { 1194 Uri uri = (Uri) parcelable; 1195 if (uri != null) { 1196 Attachment attachment = loadAttachmentInfo(uri); 1197 if (MimeUtility.mimeTypeMatches(attachment.mMimeType, 1198 Email.ACCEPTABLE_ATTACHMENT_SEND_INTENT_TYPES)) { 1199 addAttachment(attachment); 1200 } 1201 } 1202 } 1203 } 1204 } 1205 1206 // Finally - expose fields that were filled in but are normally hidden, and set focus 1207 1208 if (mCcView.length() > 0) { 1209 mCcView.setVisibility(View.VISIBLE); 1210 } 1211 if (mBccView.length() > 0) { 1212 mBccView.setVisibility(View.VISIBLE); 1213 } 1214 setNewMessageFocus(); 1215 setDraftNeedsSaving(false); 1216 } 1217 1218 /** 1219 * When we are launched with an intent that includes a mailto: URI, we can actually 1220 * gather quite a few of our message fields from it. 1221 * 1222 * @mailToString the href (which must start with "mailto:"). 1223 */ 1224 private void initializeFromMailTo(String mailToString) { 1225 1226 // Chop up everything between mailto: and ? to find recipients 1227 int index = mailToString.indexOf("?"); 1228 int length = "mailto".length() + 1; 1229 String to; 1230 try { 1231 // Extract the recipient after mailto: 1232 if (index == -1) { 1233 to = decode(mailToString.substring(length)); 1234 } else { 1235 to = decode(mailToString.substring(length, index)); 1236 } 1237 addAddresses(mToView, to.split(" ,")); 1238 } catch (UnsupportedEncodingException e) { 1239 Log.e(Email.LOG_TAG, e.getMessage() + " while decoding '" + mailToString + "'"); 1240 } 1241 1242 // Extract the other parameters 1243 1244 // We need to disguise this string as a URI in order to parse it 1245 Uri uri = Uri.parse("foo://" + mailToString); 1246 1247 List<String> cc = uri.getQueryParameters("cc"); 1248 addAddresses(mCcView, cc.toArray(new String[cc.size()])); 1249 1250 List<String> otherTo = uri.getQueryParameters("to"); 1251 addAddresses(mCcView, otherTo.toArray(new String[otherTo.size()])); 1252 1253 List<String> bcc = uri.getQueryParameters("bcc"); 1254 addAddresses(mBccView, bcc.toArray(new String[bcc.size()])); 1255 1256 List<String> subject = uri.getQueryParameters("subject"); 1257 if (subject.size() > 0) { 1258 mSubjectView.setText(subject.get(0)); 1259 } 1260 1261 List<String> body = uri.getQueryParameters("body"); 1262 if (body.size() > 0) { 1263 mMessageContentView.setText(body.get(0)); 1264 } 1265 } 1266 1267 private String decode(String s) throws UnsupportedEncodingException { 1268 return URLDecoder.decode(s, "UTF-8"); 1269 } 1270 1271 // used by processSourceMessage() 1272 private void displayQuotedText(String textBody, String htmlBody) { 1273 /* Use plain-text body if available, otherwise use HTML body. 1274 * This matches the desired behavior for IMAP/POP where we only send plain-text, 1275 * and for EAS which sends HTML and has no plain-text body. 1276 */ 1277 boolean plainTextFlag = textBody != null; 1278 String text = plainTextFlag ? textBody : htmlBody; 1279 if (text != null) { 1280 text = plainTextFlag ? EmailHtmlUtil.escapeCharacterToDisplay(text) : text; 1281 // TODO: re-enable EmailHtmlUtil.resolveInlineImage() for HTML 1282 // EmailHtmlUtil.resolveInlineImage(getContentResolver(), mAccount, 1283 // text, message, 0); 1284 mQuotedTextBar.setVisibility(View.VISIBLE); 1285 if (mQuotedText != null) { 1286 mQuotedText.setVisibility(View.VISIBLE); 1287 mQuotedText.loadDataWithBaseURL("email://", text, "text/html", "utf-8", null); 1288 } 1289 } 1290 } 1291 1292 /** 1293 * Given a packed address String, the address of our sending account, a view, and a list of 1294 * addressees already added to other addressing views, adds unique addressees that don't 1295 * match our address to the passed in view 1296 */ 1297 private boolean safeAddAddresses(String addrs, String ourAddress, 1298 MultiAutoCompleteTextView view, ArrayList<Address> addrList) { 1299 boolean added = false; 1300 for (Address address : Address.unpack(addrs)) { 1301 // Don't send to ourselves or already-included addresses 1302 if (!address.getAddress().equalsIgnoreCase(ourAddress) && !addrList.contains(address)) { 1303 addrList.add(address); 1304 addAddress(view, address.toString()); 1305 added = true; 1306 } 1307 } 1308 return added; 1309 } 1310 1311 /** 1312 * Set up the to and cc views properly for the "reply" and "replyAll" cases. What's important 1313 * is that we not 1) send to ourselves, and 2) duplicate addressees. 1314 * @param message the message we're replying to 1315 * @param account the account we're sending from 1316 * @param toView the "To" view 1317 * @param ccView the "Cc" view 1318 * @param replyAll whether this is a replyAll (vs a reply) 1319 */ 1320 /*package*/ void setupAddressViews(Message message, Account account, 1321 MultiAutoCompleteTextView toView, MultiAutoCompleteTextView ccView, boolean replyAll) { 1322 /* 1323 * If a reply-to was included with the message use that, otherwise use the from 1324 * or sender address. 1325 */ 1326 Address[] replyToAddresses = Address.unpack(message.mReplyTo); 1327 if (replyToAddresses.length == 0) { 1328 replyToAddresses = Address.unpack(message.mFrom); 1329 } 1330 addAddresses(mToView, replyToAddresses); 1331 1332 if (replyAll) { 1333 // Keep a running list of addresses we're sending to 1334 ArrayList<Address> allAddresses = new ArrayList<Address>(); 1335 String ourAddress = account.mEmailAddress; 1336 1337 for (Address address: replyToAddresses) { 1338 allAddresses.add(address); 1339 } 1340 1341 safeAddAddresses(message.mTo, ourAddress, mToView, allAddresses); 1342 if (safeAddAddresses(message.mCc, ourAddress, mCcView, allAddresses)) { 1343 mCcView.setVisibility(View.VISIBLE); 1344 } 1345 } 1346 } 1347 1348 void processSourceMessageGuarded(Message message, Account account) { 1349 // Make sure we only do this once (otherwise we'll duplicate addresses!) 1350 if (!mSourceMessageProcessed) { 1351 processSourceMessage(message, account); 1352 mSourceMessageProcessed = true; 1353 } 1354 1355 /* The quoted text is displayed in a WebView whose content is not automatically 1356 * saved/restored by onRestoreInstanceState(), so we need to *always* restore it here, 1357 * regardless of the value of mSourceMessageProcessed. 1358 * This only concerns EDIT_DRAFT because after a configuration change we're always 1359 * in EDIT_DRAFT. 1360 */ 1361 if (ACTION_EDIT_DRAFT.equals(mAction)) { 1362 displayQuotedText(message.mTextReply, message.mHtmlReply); 1363 } 1364 } 1365 1366 /** 1367 * Pull out the parts of the now loaded source message and apply them to the new message 1368 * depending on the type of message being composed. 1369 * @param message 1370 */ 1371 /* package */ 1372 void processSourceMessage(Message message, Account account) { 1373 setDraftNeedsSaving(true); 1374 final String subject = message.mSubject; 1375 if (ACTION_REPLY.equals(mAction) || ACTION_REPLY_ALL.equals(mAction)) { 1376 setupAddressViews(message, account, mToView, mCcView, 1377 ACTION_REPLY_ALL.equals(mAction)); 1378 if (subject != null && !subject.toLowerCase().startsWith("re:")) { 1379 mSubjectView.setText("Re: " + subject); 1380 } else { 1381 mSubjectView.setText(subject); 1382 } 1383 displayQuotedText(message.mText, message.mHtml); 1384 } else if (ACTION_FORWARD.equals(mAction)) { 1385 mSubjectView.setText(subject != null && !subject.toLowerCase().startsWith("fwd:") ? 1386 "Fwd: " + subject : subject); 1387 displayQuotedText(message.mText, message.mHtml); 1388 // TODO: re-enable loadAttachments below 1389// if (!loadAttachments(message, 0)) { 1390// mHandler.sendEmptyMessage(MSG_SKIPPED_ATTACHMENTS); 1391// } 1392 } else if (ACTION_EDIT_DRAFT.equals(mAction)) { 1393 mSubjectView.setText(subject); 1394 addAddresses(mToView, Address.unpack(message.mTo)); 1395 Address[] cc = Address.unpack(message.mCc); 1396 if (cc.length > 0) { 1397 addAddresses(mCcView, cc); 1398 mCcView.setVisibility(View.VISIBLE); 1399 } 1400 Address[] bcc = Address.unpack(message.mBcc); 1401 if (bcc.length > 0) { 1402 addAddresses(mBccView, bcc); 1403 mBccView.setVisibility(View.VISIBLE); 1404 } 1405 1406 mMessageContentView.setText(message.mText); 1407 // TODO: re-enable loadAttachments 1408 // loadAttachments(message, 0); 1409 setDraftNeedsSaving(false); 1410 } 1411 setNewMessageFocus(); 1412 } 1413 1414 /** 1415 * In order to accelerate typing, position the cursor in the first empty field, 1416 * or at the end of the body composition field if none are empty. Typically, this will 1417 * play out as follows: 1418 * Reply / Reply All - put cursor in the empty message body 1419 * Forward - put cursor in the empty To field 1420 * Edit Draft - put cursor in whatever field still needs entry 1421 */ 1422 private void setNewMessageFocus() { 1423 if (mToView.length() == 0) { 1424 mToView.requestFocus(); 1425 } else if (mSubjectView.length() == 0) { 1426 mSubjectView.requestFocus(); 1427 } else { 1428 mMessageContentView.requestFocus(); 1429 // when selecting the message content, explicitly move IP to the end, so you can 1430 // quickly resume typing into a draft 1431 int selection = mMessageContentView.length(); 1432 mMessageContentView.setSelection(selection, selection); 1433 } 1434 } 1435 1436 class Listener implements Controller.Result { 1437 public void updateMailboxListCallback(MessagingException result, long accountId, 1438 int progress) { 1439 } 1440 1441 public void updateMailboxCallback(MessagingException result, long accountId, 1442 long mailboxId, int progress, int numNewMessages) { 1443 if (result != null || progress == 100) { 1444 Email.updateMailboxRefreshTime(mailboxId); 1445 } 1446 } 1447 1448 public void loadMessageForViewCallback(MessagingException result, long messageId, 1449 int progress) { 1450 } 1451 1452 public void loadAttachmentCallback(MessagingException result, long messageId, 1453 long attachmentId, int progress) { 1454 } 1455 1456 public void serviceCheckMailCallback(MessagingException result, long accountId, 1457 long mailboxId, int progress, long tag) { 1458 } 1459 1460 public void sendMailCallback(MessagingException result, long accountId, long messageId, 1461 int progress) { 1462 } 1463 } 1464} 1465