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