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