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