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