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