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