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