1 /* 2 * Copyright (C) 2009 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 17 package com.android.mms.data; 18 19 import java.util.List; 20 21 import android.content.ContentResolver; 22 import android.content.ContentUris; 23 import android.content.ContentValues; 24 import android.content.Context; 25 import android.database.Cursor; 26 import android.database.sqlite.SqliteWrapper; 27 import android.net.Uri; 28 import android.os.Bundle; 29 import android.provider.Telephony.Mms; 30 import android.provider.Telephony.MmsSms; 31 import android.provider.Telephony.Sms; 32 import android.provider.Telephony.MmsSms.PendingMessages; 33 import android.telephony.SmsMessage; 34 import android.text.TextUtils; 35 import android.util.Log; 36 37 import com.android.common.userhappiness.UserHappinessSignals; 38 import com.android.mms.ExceedMessageSizeException; 39 import com.android.mms.LogTag; 40 import com.android.mms.MmsConfig; 41 import com.android.mms.ResolutionException; 42 import com.android.mms.UnsupportContentTypeException; 43 import com.android.mms.model.AudioModel; 44 import com.android.mms.model.ImageModel; 45 import com.android.mms.model.MediaModel; 46 import com.android.mms.model.SlideModel; 47 import com.android.mms.model.SlideshowModel; 48 import com.android.mms.model.TextModel; 49 import com.android.mms.model.VideoModel; 50 import com.android.mms.transaction.MessageSender; 51 import com.android.mms.transaction.MmsMessageSender; 52 import com.android.mms.transaction.SmsMessageSender; 53 import com.android.mms.ui.ComposeMessageActivity; 54 import com.android.mms.ui.MessageUtils; 55 import com.android.mms.ui.SlideshowEditor; 56 import com.android.mms.util.Recycler; 57 import com.google.android.mms.ContentType; 58 import com.google.android.mms.MmsException; 59 import com.google.android.mms.pdu.EncodedStringValue; 60 import com.google.android.mms.pdu.PduBody; 61 import com.google.android.mms.pdu.PduPersister; 62 import com.google.android.mms.pdu.SendReq; 63 64 /** 65 * Contains all state related to a message being edited by the user. 66 */ 67 public class WorkingMessage { 68 private static final String TAG = "WorkingMessage"; 69 private static final boolean DEBUG = false; 70 71 // Public intents 72 public static final String ACTION_SENDING_SMS = "android.intent.action.SENDING_SMS"; 73 74 // Intent extras 75 public static final String EXTRA_SMS_MESSAGE = "android.mms.extra.MESSAGE"; 76 public static final String EXTRA_SMS_RECIPIENTS = "android.mms.extra.RECIPIENTS"; 77 public static final String EXTRA_SMS_THREAD_ID = "android.mms.extra.THREAD_ID"; 78 79 // Database access stuff 80 private final Context mContext; 81 private final ContentResolver mContentResolver; 82 83 // States that can require us to save or send a message as MMS. 84 private static final int RECIPIENTS_REQUIRE_MMS = (1 << 0); // 1 85 private static final int HAS_SUBJECT = (1 << 1); // 2 86 private static final int HAS_ATTACHMENT = (1 << 2); // 4 87 private static final int LENGTH_REQUIRES_MMS = (1 << 3); // 8 88 private static final int FORCE_MMS = (1 << 4); // 16 89 90 // A bitmap of the above indicating different properties of the message; 91 // any bit set will require the message to be sent via MMS. 92 private int mMmsState; 93 94 // Errors from setAttachment() 95 public static final int OK = 0; 96 public static final int UNKNOWN_ERROR = -1; 97 public static final int MESSAGE_SIZE_EXCEEDED = -2; 98 public static final int UNSUPPORTED_TYPE = -3; 99 public static final int IMAGE_TOO_LARGE = -4; 100 101 // Attachment types 102 public static final int TEXT = 0; 103 public static final int IMAGE = 1; 104 public static final int VIDEO = 2; 105 public static final int AUDIO = 3; 106 public static final int SLIDESHOW = 4; 107 108 // Current attachment type of the message; one of the above values. 109 private int mAttachmentType; 110 111 // Conversation this message is targeting. 112 private Conversation mConversation; 113 114 // Text of the message. 115 private CharSequence mText; 116 // Slideshow for this message, if applicable. If it's a simple attachment, 117 // i.e. not SLIDESHOW, it will contain only one slide. 118 private SlideshowModel mSlideshow; 119 // Data URI of an MMS message if we have had to save it. 120 private Uri mMessageUri; 121 // MMS subject line for this message 122 private CharSequence mSubject; 123 124 // Set to true if this message has been discarded. 125 private boolean mDiscarded = false; 126 127 // Cached value of mms enabled flag 128 private static boolean sMmsEnabled = MmsConfig.getMmsEnabled(); 129 130 // Our callback interface 131 private final MessageStatusListener mStatusListener; 132 private List<String> mWorkingRecipients; 133 134 // Message sizes in Outbox 135 private static final String[] MMS_OUTBOX_PROJECTION = { 136 Mms._ID, // 0 137 Mms.MESSAGE_SIZE // 1 138 }; 139 140 private static final int MMS_MESSAGE_SIZE_INDEX = 1; 141 142 /** 143 * Callback interface for communicating important state changes back to 144 * ComposeMessageActivity. 145 */ 146 public interface MessageStatusListener { 147 /** 148 * Called when the protocol for sending the message changes from SMS 149 * to MMS, and vice versa. 150 * 151 * @param mms If true, it changed to MMS. If false, to SMS. 152 */ 153 void onProtocolChanged(boolean mms); 154 155 /** 156 * Called when an attachment on the message has changed. 157 */ 158 void onAttachmentChanged(); 159 160 /** 161 * Called just before the process of sending a message. 162 */ 163 void onPreMessageSent(); 164 165 /** 166 * Called once the process of sending a message, triggered by 167 * {@link send} has completed. This doesn't mean the send succeeded, 168 * just that it has been dispatched to the network. 169 */ 170 void onMessageSent(); 171 172 /** 173 * Called if there are too many unsent messages in the queue and we're not allowing 174 * any more Mms's to be sent. 175 */ 176 void onMaxPendingMessagesReached(); 177 178 /** 179 * Called if there's an attachment error while resizing the images just before sending. 180 */ 181 void onAttachmentError(int error); 182 } 183 184 private WorkingMessage(ComposeMessageActivity activity) { 185 mContext = activity; 186 mContentResolver = mContext.getContentResolver(); 187 mStatusListener = activity; 188 mAttachmentType = TEXT; 189 mText = ""; 190 } 191 192 /** 193 * Creates a new working message. 194 */ 195 public static WorkingMessage createEmpty(ComposeMessageActivity activity) { 196 // Make a new empty working message. 197 WorkingMessage msg = new WorkingMessage(activity); 198 return msg; 199 } 200 201 /** 202 * Create a new WorkingMessage from the specified data URI, which typically 203 * contains an MMS message. 204 */ 205 public static WorkingMessage load(ComposeMessageActivity activity, Uri uri) { 206 // If the message is not already in the draft box, move it there. 207 if (!uri.toString().startsWith(Mms.Draft.CONTENT_URI.toString())) { 208 PduPersister persister = PduPersister.getPduPersister(activity); 209 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 210 LogTag.debug("load: moving %s to drafts", uri); 211 } 212 try { 213 uri = persister.move(uri, Mms.Draft.CONTENT_URI); 214 } catch (MmsException e) { 215 LogTag.error("Can't move %s to drafts", uri); 216 return null; 217 } 218 } 219 220 WorkingMessage msg = new WorkingMessage(activity); 221 if (msg.loadFromUri(uri)) { 222 return msg; 223 } 224 225 return null; 226 } 227 228 private void correctAttachmentState() { 229 int slideCount = mSlideshow.size(); 230 231 // If we get an empty slideshow, tear down all MMS 232 // state and discard the unnecessary message Uri. 233 if (slideCount == 0) { 234 mAttachmentType = TEXT; 235 mSlideshow = null; 236 asyncDelete(mMessageUri, null, null); 237 mMessageUri = null; 238 } else if (slideCount > 1) { 239 mAttachmentType = SLIDESHOW; 240 } else { 241 SlideModel slide = mSlideshow.get(0); 242 if (slide.hasImage()) { 243 mAttachmentType = IMAGE; 244 } else if (slide.hasVideo()) { 245 mAttachmentType = VIDEO; 246 } else if (slide.hasAudio()) { 247 mAttachmentType = AUDIO; 248 } 249 } 250 251 updateState(HAS_ATTACHMENT, hasAttachment(), false); 252 } 253 254 private boolean loadFromUri(Uri uri) { 255 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) LogTag.debug("loadFromUri %s", uri); 256 try { 257 mSlideshow = SlideshowModel.createFromMessageUri(mContext, uri); 258 } catch (MmsException e) { 259 LogTag.error("Couldn't load URI %s", uri); 260 return false; 261 } 262 263 mMessageUri = uri; 264 265 // Make sure all our state is as expected. 266 syncTextFromSlideshow(); 267 correctAttachmentState(); 268 269 return true; 270 } 271 272 /** 273 * Load the draft message for the specified conversation, or a new empty message if 274 * none exists. 275 */ 276 public static WorkingMessage loadDraft(ComposeMessageActivity activity, 277 Conversation conv) { 278 WorkingMessage msg = new WorkingMessage(activity); 279 if (msg.loadFromConversation(conv)) { 280 return msg; 281 } else { 282 return createEmpty(activity); 283 } 284 } 285 286 private boolean loadFromConversation(Conversation conv) { 287 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) LogTag.debug("loadFromConversation %s", conv); 288 289 long threadId = conv.getThreadId(); 290 if (threadId <= 0) { 291 return false; 292 } 293 294 // Look for an SMS draft first. 295 mText = readDraftSmsMessage(conv); 296 if (!TextUtils.isEmpty(mText)) { 297 return true; 298 } 299 300 // Then look for an MMS draft. 301 StringBuilder sb = new StringBuilder(); 302 Uri uri = readDraftMmsMessage(mContext, threadId, sb); 303 if (uri != null) { 304 if (loadFromUri(uri)) { 305 // If there was an MMS message, readDraftMmsMessage 306 // will put the subject in our supplied StringBuilder. 307 if (sb.length() > 0) { 308 setSubject(sb.toString(), false); 309 } 310 return true; 311 } 312 } 313 314 return false; 315 } 316 317 /** 318 * Sets the text of the message to the specified CharSequence. 319 */ 320 public void setText(CharSequence s) { 321 mText = s; 322 } 323 324 /** 325 * Returns the current message text. 326 */ 327 public CharSequence getText() { 328 return mText; 329 } 330 331 /** 332 * Returns true if the message has any text. A message with just whitespace is not considered 333 * to have text. 334 * @return 335 */ 336 public boolean hasText() { 337 return mText != null && TextUtils.getTrimmedLength(mText) > 0; 338 } 339 340 /** 341 * Adds an attachment to the message, replacing an old one if it existed. 342 * @param type Type of this attachment, such as {@link IMAGE} 343 * @param dataUri Uri containing the attachment data (or null for {@link TEXT}) 344 * @param append true if we should add the attachment to a new slide 345 * @return An error code such as {@link UNKNOWN_ERROR} or {@link OK} if successful 346 */ 347 public int setAttachment(int type, Uri dataUri, boolean append) { 348 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 349 LogTag.debug("setAttachment type=%d uri %s", type, dataUri); 350 } 351 int result = OK; 352 353 // Make sure mSlideshow is set up and has a slide. 354 ensureSlideshow(); 355 356 // Change the attachment and translate the various underlying 357 // exceptions into useful error codes. 358 try { 359 if (append) { 360 appendMedia(type, dataUri); 361 } else { 362 changeMedia(type, dataUri); 363 } 364 } catch (MmsException e) { 365 result = UNKNOWN_ERROR; 366 } catch (UnsupportContentTypeException e) { 367 result = UNSUPPORTED_TYPE; 368 } catch (ExceedMessageSizeException e) { 369 result = MESSAGE_SIZE_EXCEEDED; 370 } catch (ResolutionException e) { 371 result = IMAGE_TOO_LARGE; 372 } 373 374 // If we were successful, update mAttachmentType and notify 375 // the listener than there was a change. 376 if (result == OK) { 377 mAttachmentType = type; 378 mStatusListener.onAttachmentChanged(); 379 } else if (append) { 380 // We added a new slide and what we attempted to insert on the slide failed. 381 // Delete that slide, otherwise we could end up with a bunch of blank slides. 382 SlideshowEditor slideShowEditor = new SlideshowEditor(mContext, mSlideshow); 383 slideShowEditor.removeSlide(mSlideshow.size() - 1); 384 } 385 386 // Set HAS_ATTACHMENT if we need it. 387 updateState(HAS_ATTACHMENT, hasAttachment(), true); 388 correctAttachmentState(); 389 return result; 390 } 391 392 /** 393 * Returns true if this message contains anything worth saving. 394 */ 395 public boolean isWorthSaving() { 396 // If it actually contains anything, it's of course not empty. 397 if (hasText() || hasSubject() || hasAttachment() || hasSlideshow()) { 398 return true; 399 } 400 401 // When saveAsMms() has been called, we set FORCE_MMS to represent 402 // sort of an "invisible attachment" so that the message isn't thrown 403 // away when we are shipping it off to other activities. 404 if (isFakeMmsForDraft()) { 405 return true; 406 } 407 408 return false; 409 } 410 411 /** 412 * Returns true if FORCE_MMS is set. 413 * When saveAsMms() has been called, we set FORCE_MMS to represent 414 * sort of an "invisible attachment" so that the message isn't thrown 415 * away when we are shipping it off to other activities. 416 */ 417 public boolean isFakeMmsForDraft() { 418 return (mMmsState & FORCE_MMS) > 0; 419 } 420 421 /** 422 * Makes sure mSlideshow is set up. 423 */ 424 private void ensureSlideshow() { 425 if (mSlideshow != null) { 426 return; 427 } 428 429 SlideshowModel slideshow = SlideshowModel.createNew(mContext); 430 SlideModel slide = new SlideModel(slideshow); 431 slideshow.add(slide); 432 433 mSlideshow = slideshow; 434 } 435 436 /** 437 * Change the message's attachment to the data in the specified Uri. 438 * Used only for single-slide ("attachment mode") messages. 439 */ 440 private void changeMedia(int type, Uri uri) throws MmsException { 441 SlideModel slide = mSlideshow.get(0); 442 MediaModel media; 443 444 if (slide == null) { 445 Log.w(LogTag.TAG, "[WorkingMessage] changeMedia: no slides!"); 446 return; 447 } 448 449 // Remove any previous attachments. 450 slide.removeImage(); 451 slide.removeVideo(); 452 slide.removeAudio(); 453 454 // If we're changing to text, just bail out. 455 if (type == TEXT) { 456 return; 457 } 458 459 // Make a correct MediaModel for the type of attachment. 460 if (type == IMAGE) { 461 media = new ImageModel(mContext, uri, mSlideshow.getLayout().getImageRegion()); 462 } else if (type == VIDEO) { 463 media = new VideoModel(mContext, uri, mSlideshow.getLayout().getImageRegion()); 464 } else if (type == AUDIO) { 465 media = new AudioModel(mContext, uri); 466 } else { 467 throw new IllegalArgumentException("changeMedia type=" + type + ", uri=" + uri); 468 } 469 470 // Add it to the slide. 471 slide.add(media); 472 473 // For video and audio, set the duration of the slide to 474 // that of the attachment. 475 if (type == VIDEO || type == AUDIO) { 476 slide.updateDuration(media.getDuration()); 477 } 478 } 479 480 /** 481 * Add the message's attachment to the data in the specified Uri to a new slide. 482 */ 483 private void appendMedia(int type, Uri uri) throws MmsException { 484 485 // If we're changing to text, just bail out. 486 if (type == TEXT) { 487 return; 488 } 489 490 // The first time this method is called, mSlideshow.size() is going to be 491 // one (a newly initialized slideshow has one empty slide). The first time we 492 // attach the picture/video to that first empty slide. From then on when this 493 // function is called, we've got to create a new slide and add the picture/video 494 // to that new slide. 495 boolean addNewSlide = true; 496 if (mSlideshow.size() == 1 && !mSlideshow.isSimple()) { 497 addNewSlide = false; 498 } 499 if (addNewSlide) { 500 SlideshowEditor slideShowEditor = new SlideshowEditor(mContext, mSlideshow); 501 if (!slideShowEditor.addNewSlide()) { 502 return; 503 } 504 } 505 // Make a correct MediaModel for the type of attachment. 506 MediaModel media; 507 SlideModel slide = mSlideshow.get(mSlideshow.size() - 1); 508 if (type == IMAGE) { 509 media = new ImageModel(mContext, uri, mSlideshow.getLayout().getImageRegion()); 510 } else if (type == VIDEO) { 511 media = new VideoModel(mContext, uri, mSlideshow.getLayout().getImageRegion()); 512 } else if (type == AUDIO) { 513 media = new AudioModel(mContext, uri); 514 } else { 515 throw new IllegalArgumentException("changeMedia type=" + type + ", uri=" + uri); 516 } 517 518 // Add it to the slide. 519 slide.add(media); 520 521 // For video and audio, set the duration of the slide to 522 // that of the attachment. 523 if (type == VIDEO || type == AUDIO) { 524 slide.updateDuration(media.getDuration()); 525 } 526 } 527 528 /** 529 * Returns true if the message has an attachment (including slideshows). 530 */ 531 public boolean hasAttachment() { 532 return (mAttachmentType > TEXT); 533 } 534 535 /** 536 * Returns the slideshow associated with this message. 537 */ 538 public SlideshowModel getSlideshow() { 539 return mSlideshow; 540 } 541 542 /** 543 * Returns true if the message has a real slideshow, as opposed to just 544 * one image attachment, for example. 545 */ 546 public boolean hasSlideshow() { 547 return (mAttachmentType == SLIDESHOW); 548 } 549 550 /** 551 * Sets the MMS subject of the message. Passing null indicates that there 552 * is no subject. Passing "" will result in an empty subject being added 553 * to the message, possibly triggering a conversion to MMS. This extra 554 * bit of state is needed to support ComposeMessageActivity converting to 555 * MMS when the user adds a subject. An empty subject will be removed 556 * before saving to disk or sending, however. 557 */ 558 public void setSubject(CharSequence s, boolean notify) { 559 mSubject = s; 560 updateState(HAS_SUBJECT, (s != null), notify); 561 } 562 563 /** 564 * Returns the MMS subject of the message. 565 */ 566 public CharSequence getSubject() { 567 return mSubject; 568 } 569 570 /** 571 * Returns true if this message has an MMS subject. A subject has to be more than just 572 * whitespace. 573 * @return 574 */ 575 public boolean hasSubject() { 576 return mSubject != null && TextUtils.getTrimmedLength(mSubject) > 0; 577 } 578 579 /** 580 * Moves the message text into the slideshow. Should be called any time 581 * the message is about to be sent or written to disk. 582 */ 583 private void syncTextToSlideshow() { 584 if (mSlideshow == null || mSlideshow.size() != 1) 585 return; 586 587 SlideModel slide = mSlideshow.get(0); 588 TextModel text; 589 if (!slide.hasText()) { 590 // Add a TextModel to slide 0 if one doesn't already exist 591 text = new TextModel(mContext, ContentType.TEXT_PLAIN, "text_0.txt", 592 mSlideshow.getLayout().getTextRegion()); 593 slide.add(text); 594 } else { 595 // Otherwise just reuse the existing one. 596 text = slide.getText(); 597 } 598 text.setText(mText); 599 } 600 601 /** 602 * Sets the message text out of the slideshow. Should be called any time 603 * a slideshow is loaded from disk. 604 */ 605 private void syncTextFromSlideshow() { 606 // Don't sync text for real slideshows. 607 if (mSlideshow.size() != 1) { 608 return; 609 } 610 611 SlideModel slide = mSlideshow.get(0); 612 if (slide == null || !slide.hasText()) { 613 return; 614 } 615 616 mText = slide.getText().getText(); 617 } 618 619 /** 620 * Removes the subject if it is empty, possibly converting back to SMS. 621 */ 622 private void removeSubjectIfEmpty(boolean notify) { 623 if (!hasSubject()) { 624 setSubject(null, notify); 625 } 626 } 627 628 /** 629 * Gets internal message state ready for storage. Should be called any 630 * time the message is about to be sent or written to disk. 631 */ 632 private void prepareForSave(boolean notify) { 633 // Make sure our working set of recipients is resolved 634 // to first-class Contact objects before we save. 635 syncWorkingRecipients(); 636 637 if (requiresMms()) { 638 ensureSlideshow(); 639 syncTextToSlideshow(); 640 removeSubjectIfEmpty(notify); 641 } 642 } 643 644 /** 645 * Resolve the temporary working set of recipients to a ContactList. 646 */ 647 public void syncWorkingRecipients() { 648 if (mWorkingRecipients != null) { 649 ContactList recipients = ContactList.getByNumbers(mWorkingRecipients, false); 650 mConversation.setRecipients(recipients); 651 mWorkingRecipients = null; 652 } 653 } 654 655 // Call when we've returned from adding an attachment. We're no longer forcing the message 656 // into a Mms message. At this point we either have the goods to make the message a Mms 657 // or we don't. No longer fake it. 658 public void removeFakeMmsForDraft() { 659 updateState(FORCE_MMS, false, false); 660 } 661 662 /** 663 * Force the message to be saved as MMS and return the Uri of the message. 664 * Typically used when handing a message off to another activity. 665 */ 666 public Uri saveAsMms(boolean notify) { 667 if (DEBUG) LogTag.debug("save mConversation=%s", mConversation); 668 669 if (mDiscarded) { 670 throw new IllegalStateException("save() called after discard()"); 671 } 672 673 // FORCE_MMS behaves as sort of an "invisible attachment", making 674 // the message seem non-empty (and thus not discarded). This bit 675 // is sticky until the last other MMS bit is removed, at which 676 // point the message will fall back to SMS. 677 updateState(FORCE_MMS, true, notify); 678 679 // Collect our state to be written to disk. 680 prepareForSave(true /* notify */); 681 682 // Make sure we are saving to the correct thread ID. 683 mConversation.ensureThreadId(); 684 mConversation.setDraftState(true); 685 686 PduPersister persister = PduPersister.getPduPersister(mContext); 687 SendReq sendReq = makeSendReq(mConversation, mSubject); 688 689 // If we don't already have a Uri lying around, make a new one. If we do 690 // have one already, make sure it is synced to disk. 691 if (mMessageUri == null) { 692 mMessageUri = createDraftMmsMessage(persister, sendReq, mSlideshow); 693 } else { 694 updateDraftMmsMessage(mMessageUri, persister, mSlideshow, sendReq); 695 } 696 697 return mMessageUri; 698 } 699 700 /** 701 * Save this message as a draft in the conversation previously specified 702 * to {@link setConversation}. 703 */ 704 public void saveDraft() { 705 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 706 LogTag.debug("saveDraft"); 707 } 708 709 // If we have discarded the message, just bail out. 710 if (mDiscarded) { 711 return; 712 } 713 714 // Make sure setConversation was called. 715 if (mConversation == null) { 716 throw new IllegalStateException("saveDraft() called with no conversation"); 717 } 718 719 // Get ready to write to disk. But don't notify message status when saving draft 720 prepareForSave(false /* notify */); 721 722 if (requiresMms()) { 723 asyncUpdateDraftMmsMessage(mConversation); 724 } else { 725 String content = mText.toString(); 726 727 // bug 2169583: don't bother creating a thread id only to delete the thread 728 // because the content is empty. When we delete the thread in updateDraftSmsMessage, 729 // we didn't nullify conv.mThreadId, causing a temperary situation where conv 730 // is holding onto a thread id that isn't in the database. If a new message arrives 731 // and takes that thread id (because it's the next thread id to be assigned), the 732 // new message will be merged with the draft message thread, causing confusion! 733 if (!TextUtils.isEmpty(content)) { 734 asyncUpdateDraftSmsMessage(mConversation, content); 735 } 736 } 737 738 // Update state of the draft cache. 739 mConversation.setDraftState(true); 740 } 741 742 synchronized public void discard() { 743 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 744 LogTag.debug("discard"); 745 } 746 747 if (mDiscarded == true) { 748 return; 749 } 750 751 // Mark this message as discarded in order to make saveDraft() no-op. 752 mDiscarded = true; 753 754 // Delete our MMS message, if there is one. 755 if (mMessageUri != null) { 756 asyncDelete(mMessageUri, null, null); 757 } 758 759 // Delete any draft messages associated with this conversation. 760 asyncDeleteDraftSmsMessage(mConversation); 761 762 // Update state of the draft cache. 763 mConversation.setDraftState(false); 764 } 765 766 public void unDiscard() { 767 if (DEBUG) LogTag.debug("unDiscard"); 768 769 mDiscarded = false; 770 } 771 772 /** 773 * Returns true if discard() has been called on this message. 774 */ 775 public boolean isDiscarded() { 776 return mDiscarded; 777 } 778 779 /** 780 * To be called from our Activity's onSaveInstanceState() to give us a chance 781 * to stow our state away for later retrieval. 782 * 783 * @param bundle The Bundle passed in to onSaveInstanceState 784 */ 785 public void writeStateToBundle(Bundle bundle) { 786 if (hasSubject()) { 787 bundle.putString("subject", mSubject.toString()); 788 } 789 790 if (mMessageUri != null) { 791 bundle.putParcelable("msg_uri", mMessageUri); 792 } else if (hasText()) { 793 bundle.putString("sms_body", mText.toString()); 794 } 795 } 796 797 /** 798 * To be called from our Activity's onCreate() if the activity manager 799 * has given it a Bundle to reinflate 800 * @param bundle The Bundle passed in to onCreate 801 */ 802 public void readStateFromBundle(Bundle bundle) { 803 if (bundle == null) { 804 return; 805 } 806 807 String subject = bundle.getString("subject"); 808 setSubject(subject, false); 809 810 Uri uri = (Uri)bundle.getParcelable("msg_uri"); 811 if (uri != null) { 812 loadFromUri(uri); 813 return; 814 } else { 815 String body = bundle.getString("sms_body"); 816 mText = body; 817 } 818 } 819 820 /** 821 * Update the temporary list of recipients, used when setting up a 822 * new conversation. Will be converted to a ContactList on any 823 * save event (send, save draft, etc.) 824 */ 825 public void setWorkingRecipients(List<String> numbers) { 826 mWorkingRecipients = numbers; 827 } 828 829 /** 830 * Set the conversation associated with this message. 831 */ 832 public void setConversation(Conversation conv) { 833 if (DEBUG) LogTag.debug("setConversation %s -> %s", mConversation, conv); 834 835 mConversation = conv; 836 837 // Convert to MMS if there are any email addresses in the recipient list. 838 setHasEmail(conv.getRecipients().containsEmail(), false); 839 } 840 841 /** 842 * Hint whether or not this message will be delivered to an 843 * an email address. 844 */ 845 public void setHasEmail(boolean hasEmail, boolean notify) { 846 if (MmsConfig.getEmailGateway() != null) { 847 updateState(RECIPIENTS_REQUIRE_MMS, false, notify); 848 } else { 849 updateState(RECIPIENTS_REQUIRE_MMS, hasEmail, notify); 850 } 851 } 852 853 /** 854 * Returns true if this message would require MMS to send. 855 */ 856 public boolean requiresMms() { 857 return (mMmsState > 0); 858 } 859 860 private static String stateString(int state) { 861 if (state == 0) 862 return "<none>"; 863 864 StringBuilder sb = new StringBuilder(); 865 if ((state & RECIPIENTS_REQUIRE_MMS) > 0) 866 sb.append("RECIPIENTS_REQUIRE_MMS | "); 867 if ((state & HAS_SUBJECT) > 0) 868 sb.append("HAS_SUBJECT | "); 869 if ((state & HAS_ATTACHMENT) > 0) 870 sb.append("HAS_ATTACHMENT | "); 871 if ((state & LENGTH_REQUIRES_MMS) > 0) 872 sb.append("LENGTH_REQUIRES_MMS | "); 873 if ((state & FORCE_MMS) > 0) 874 sb.append("FORCE_MMS | "); 875 876 sb.delete(sb.length() - 3, sb.length()); 877 return sb.toString(); 878 } 879 880 /** 881 * Sets the current state of our various "MMS required" bits. 882 * 883 * @param state The bit to change, such as {@link HAS_ATTACHMENT} 884 * @param on If true, set it; if false, clear it 885 * @param notify Whether or not to notify the user 886 */ 887 private void updateState(int state, boolean on, boolean notify) { 888 if (!sMmsEnabled) { 889 // If Mms isn't enabled, the rest of the Messaging UI should not be using any 890 // feature that would cause us to to turn on any Mms flag and show the 891 // "Converting to multimedia..." message. 892 return; 893 } 894 int oldState = mMmsState; 895 if (on) { 896 mMmsState |= state; 897 } else { 898 mMmsState &= ~state; 899 } 900 901 // If we are clearing the last bit that is not FORCE_MMS, 902 // expire the FORCE_MMS bit. 903 if (mMmsState == FORCE_MMS && ((oldState & ~FORCE_MMS) > 0)) { 904 mMmsState = 0; 905 } 906 907 // Notify the listener if we are moving from SMS to MMS 908 // or vice versa. 909 if (notify) { 910 if (oldState == 0 && mMmsState != 0) { 911 mStatusListener.onProtocolChanged(true); 912 } else if (oldState != 0 && mMmsState == 0) { 913 mStatusListener.onProtocolChanged(false); 914 } 915 } 916 917 if (oldState != mMmsState) { 918 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) LogTag.debug("updateState: %s%s = %s", 919 on ? "+" : "-", 920 stateString(state), stateString(mMmsState)); 921 } 922 } 923 924 /** 925 * Send this message over the network. Will call back with onMessageSent() once 926 * it has been dispatched to the telephony stack. This WorkingMessage object is 927 * no longer useful after this method has been called. 928 */ 929 public void send() { 930 if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) { 931 LogTag.debug("send"); 932 } 933 934 // Get ready to write to disk. 935 prepareForSave(true /* notify */); 936 937 // We need the recipient list for both SMS and MMS. 938 final Conversation conv = mConversation; 939 String msgTxt = mText.toString(); 940 941 if (requiresMms() || addressContainsEmailToMms(conv, msgTxt)) { 942 // Make local copies of the bits we need for sending a message, 943 // because we will be doing it off of the main thread, which will 944 // immediately continue on to resetting some of this state. 945 final Uri mmsUri = mMessageUri; 946 final PduPersister persister = PduPersister.getPduPersister(mContext); 947 948 final SlideshowModel slideshow = mSlideshow; 949 final SendReq sendReq = makeSendReq(conv, mSubject); 950 951 // Do the dirty work of sending the message off of the main UI thread. 952 new Thread(new Runnable() { 953 public void run() { 954 // Make sure the text in slide 0 is no longer holding onto a reference to 955 // the text in the message text box. 956 slideshow.prepareForSend(); 957 sendMmsWorker(conv, mmsUri, persister, slideshow, sendReq); 958 } 959 }).start(); 960 } else { 961 // Same rules apply as above. 962 final String msgText = mText.toString(); 963 new Thread(new Runnable() { 964 public void run() { 965 preSendSmsWorker(conv, msgText); 966 } 967 }).start(); 968 } 969 970 // update the Recipient cache with the new to address, if it's different 971 RecipientIdCache.updateNumbers(conv.getThreadId(), conv.getRecipients()); 972 973 // Mark the message as discarded because it is "off the market" after being sent. 974 mDiscarded = true; 975 } 976 977 private boolean addressContainsEmailToMms(Conversation conv, String text) { 978 if (MmsConfig.getEmailGateway() != null) { 979 String[] dests = conv.getRecipients().getNumbers(); 980 int length = dests.length; 981 for (int i = 0; i < length; i++) { 982 if (Mms.isEmailAddress(dests[i]) || MessageUtils.isAlias(dests[i])) { 983 String mtext = dests[i] + " " + text; 984 int[] params = SmsMessage.calculateLength(mtext, false); 985 if (params[0] > 1) { 986 updateState(RECIPIENTS_REQUIRE_MMS, true, true); 987 ensureSlideshow(); 988 syncTextToSlideshow(); 989 return true; 990 } 991 } 992 } 993 } 994 return false; 995 } 996 997 // Message sending stuff 998 999 private void preSendSmsWorker(Conversation conv, String msgText) { 1000 // If user tries to send the message, it's a signal the inputted text is what they wanted. 1001 UserHappinessSignals.userAcceptedImeText(mContext); 1002 1003 mStatusListener.onPreMessageSent(); 1004 1005 // Make sure we are still using the correct thread ID for our 1006 // recipient set. 1007 long threadId = conv.ensureThreadId(); 1008 1009 final String semiSepRecipients = conv.getRecipients().serialize(); 1010 1011 // just do a regular send. We're already on a non-ui thread so no need to fire 1012 // off another thread to do this work. 1013 sendSmsWorker(msgText, semiSepRecipients, threadId); 1014 1015 // Be paranoid and clean any draft SMS up. 1016 deleteDraftSmsMessage(threadId); 1017 } 1018 1019 private void sendSmsWorker(String msgText, String semiSepRecipients, long threadId) { 1020 String[] dests = TextUtils.split(semiSepRecipients, ";"); 1021 if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) { 1022 LogTag.debug("sendSmsWorker sending message"); 1023 } 1024 MessageSender sender = new SmsMessageSender(mContext, dests, msgText, threadId); 1025 try { 1026 sender.sendMessage(threadId); 1027 1028 // Make sure this thread isn't over the limits in message count 1029 Recycler.getSmsRecycler().deleteOldMessagesByThreadId(mContext, threadId); 1030 } catch (Exception e) { 1031 Log.e(TAG, "Failed to send SMS message, threadId=" + threadId, e); 1032 } 1033 1034 mStatusListener.onMessageSent(); 1035 } 1036 1037 private void sendMmsWorker(Conversation conv, Uri mmsUri, PduPersister persister, 1038 SlideshowModel slideshow, SendReq sendReq) { 1039 // If user tries to send the message, it's a signal the inputted text is what they wanted. 1040 UserHappinessSignals.userAcceptedImeText(mContext); 1041 1042 // First make sure we don't have too many outstanding unsent message. 1043 Cursor cursor = null; 1044 try { 1045 cursor = SqliteWrapper.query(mContext, mContentResolver, 1046 Mms.Outbox.CONTENT_URI, MMS_OUTBOX_PROJECTION, null, null, null); 1047 if (cursor != null) { 1048 long maxMessageSize = MmsConfig.getMaxSizeScaleForPendingMmsAllowed() * 1049 MmsConfig.getMaxMessageSize(); 1050 long totalPendingSize = 0; 1051 while (cursor.moveToNext()) { 1052 totalPendingSize += cursor.getLong(MMS_MESSAGE_SIZE_INDEX); 1053 } 1054 if (totalPendingSize >= maxMessageSize) { 1055 unDiscard(); // it wasn't successfully sent. Allow it to be saved as a draft. 1056 mStatusListener.onMaxPendingMessagesReached(); 1057 return; 1058 } 1059 } 1060 } finally { 1061 if (cursor != null) { 1062 cursor.close(); 1063 } 1064 } 1065 mStatusListener.onPreMessageSent(); 1066 1067 // Make sure we are still using the correct thread ID for our 1068 // recipient set. 1069 long threadId = conv.ensureThreadId(); 1070 1071 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 1072 LogTag.debug("sendMmsWorker: update draft MMS message " + mmsUri); 1073 } 1074 1075 if (mmsUri == null) { 1076 // Create a new MMS message if one hasn't been made yet. 1077 mmsUri = createDraftMmsMessage(persister, sendReq, slideshow); 1078 } else { 1079 // Otherwise, sync the MMS message in progress to disk. 1080 updateDraftMmsMessage(mmsUri, persister, slideshow, sendReq); 1081 } 1082 1083 // Be paranoid and clean any draft SMS up. 1084 deleteDraftSmsMessage(threadId); 1085 1086 // Resize all the resizeable attachments (e.g. pictures) to fit 1087 // in the remaining space in the slideshow. 1088 int error = 0; 1089 try { 1090 slideshow.finalResize(mmsUri); 1091 } catch (ExceedMessageSizeException e1) { 1092 error = MESSAGE_SIZE_EXCEEDED; 1093 } catch (MmsException e1) { 1094 error = UNKNOWN_ERROR; 1095 } 1096 if (error != 0) { 1097 markMmsMessageWithError(mmsUri); 1098 mStatusListener.onAttachmentError(error); 1099 return; 1100 } 1101 1102 MessageSender sender = new MmsMessageSender(mContext, mmsUri, 1103 slideshow.getCurrentMessageSize()); 1104 try { 1105 if (!sender.sendMessage(threadId)) { 1106 // The message was sent through SMS protocol, we should 1107 // delete the copy which was previously saved in MMS drafts. 1108 SqliteWrapper.delete(mContext, mContentResolver, mmsUri, null, null); 1109 } 1110 1111 // Make sure this thread isn't over the limits in message count 1112 Recycler.getMmsRecycler().deleteOldMessagesByThreadId(mContext, threadId); 1113 } catch (Exception e) { 1114 Log.e(TAG, "Failed to send message: " + mmsUri + ", threadId=" + threadId, e); 1115 } 1116 1117 mStatusListener.onMessageSent(); 1118 } 1119 1120 private void markMmsMessageWithError(Uri mmsUri) { 1121 try { 1122 PduPersister p = PduPersister.getPduPersister(mContext); 1123 // Move the message into MMS Outbox. A trigger will create an entry in 1124 // the "pending_msgs" table. 1125 p.move(mmsUri, Mms.Outbox.CONTENT_URI); 1126 1127 // Now update the pending_msgs table with an error for that new item. 1128 ContentValues values = new ContentValues(1); 1129 values.put(PendingMessages.ERROR_TYPE, MmsSms.ERR_TYPE_GENERIC_PERMANENT); 1130 long msgId = ContentUris.parseId(mmsUri); 1131 SqliteWrapper.update(mContext, mContentResolver, 1132 PendingMessages.CONTENT_URI, 1133 values, PendingMessages._ID + "=" + msgId, null); 1134 } catch (MmsException e) { 1135 // Not much we can do here. If the p.move throws an exception, we'll just 1136 // leave the message in the draft box. 1137 Log.e(TAG, "Failed to move message to outbox and mark as error: " + mmsUri, e); 1138 } 1139 } 1140 1141 // Draft message stuff 1142 1143 private static final String[] MMS_DRAFT_PROJECTION = { 1144 Mms._ID, // 0 1145 Mms.SUBJECT, // 1 1146 Mms.SUBJECT_CHARSET // 2 1147 }; 1148 1149 private static final int MMS_ID_INDEX = 0; 1150 private static final int MMS_SUBJECT_INDEX = 1; 1151 private static final int MMS_SUBJECT_CS_INDEX = 2; 1152 1153 private static Uri readDraftMmsMessage(Context context, long threadId, StringBuilder sb) { 1154 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 1155 LogTag.debug("readDraftMmsMessage tid=%d", threadId); 1156 } 1157 Cursor cursor; 1158 ContentResolver cr = context.getContentResolver(); 1159 1160 final String selection = Mms.THREAD_ID + " = " + threadId; 1161 cursor = SqliteWrapper.query(context, cr, 1162 Mms.Draft.CONTENT_URI, MMS_DRAFT_PROJECTION, 1163 selection, null, null); 1164 1165 Uri uri; 1166 try { 1167 if (cursor.moveToFirst()) { 1168 uri = ContentUris.withAppendedId(Mms.Draft.CONTENT_URI, 1169 cursor.getLong(MMS_ID_INDEX)); 1170 String subject = MessageUtils.extractEncStrFromCursor(cursor, MMS_SUBJECT_INDEX, 1171 MMS_SUBJECT_CS_INDEX); 1172 if (subject != null) { 1173 sb.append(subject); 1174 } 1175 return uri; 1176 } 1177 } finally { 1178 cursor.close(); 1179 } 1180 1181 return null; 1182 } 1183 1184 /** 1185 * makeSendReq should always return a non-null SendReq, whether the dest addresses are 1186 * valid or not. 1187 */ 1188 private static SendReq makeSendReq(Conversation conv, CharSequence subject) { 1189 String[] dests = conv.getRecipients().getNumbers(true /* scrub for MMS address */); 1190 1191 SendReq req = new SendReq(); 1192 EncodedStringValue[] encodedNumbers = EncodedStringValue.encodeStrings(dests); 1193 if (encodedNumbers != null) { 1194 req.setTo(encodedNumbers); 1195 } 1196 1197 if (!TextUtils.isEmpty(subject)) { 1198 req.setSubject(new EncodedStringValue(subject.toString())); 1199 } 1200 1201 req.setDate(System.currentTimeMillis() / 1000L); 1202 1203 return req; 1204 } 1205 1206 private static Uri createDraftMmsMessage(PduPersister persister, SendReq sendReq, 1207 SlideshowModel slideshow) { 1208 try { 1209 PduBody pb = slideshow.toPduBody(); 1210 sendReq.setBody(pb); 1211 Uri res = persister.persist(sendReq, Mms.Draft.CONTENT_URI); 1212 slideshow.sync(pb); 1213 return res; 1214 } catch (MmsException e) { 1215 return null; 1216 } 1217 } 1218 1219 private void asyncUpdateDraftMmsMessage(final Conversation conv) { 1220 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 1221 LogTag.debug("asyncUpdateDraftMmsMessage conv=%s mMessageUri=%s", conv, mMessageUri); 1222 } 1223 1224 final PduPersister persister = PduPersister.getPduPersister(mContext); 1225 final SendReq sendReq = makeSendReq(conv, mSubject); 1226 1227 new Thread(new Runnable() { 1228 public void run() { 1229 conv.ensureThreadId(); 1230 conv.setDraftState(true); 1231 if (mMessageUri == null) { 1232 mMessageUri = createDraftMmsMessage(persister, sendReq, mSlideshow); 1233 } else { 1234 updateDraftMmsMessage(mMessageUri, persister, mSlideshow, sendReq); 1235 } 1236 1237 // Be paranoid and delete any SMS drafts that might be lying around. Must do 1238 // this after ensureThreadId so conv has the correct thread id. 1239 asyncDeleteDraftSmsMessage(conv); 1240 } 1241 }).start(); 1242 } 1243 1244 private static void updateDraftMmsMessage(Uri uri, PduPersister persister, 1245 SlideshowModel slideshow, SendReq sendReq) { 1246 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 1247 LogTag.debug("updateDraftMmsMessage uri=%s", uri); 1248 } 1249 if (uri == null) { 1250 Log.e(TAG, "updateDraftMmsMessage null uri"); 1251 return; 1252 } 1253 persister.updateHeaders(uri, sendReq); 1254 final PduBody pb = slideshow.toPduBody(); 1255 1256 try { 1257 persister.updateParts(uri, pb); 1258 } catch (MmsException e) { 1259 Log.e(TAG, "updateDraftMmsMessage: cannot update message " + uri); 1260 } 1261 1262 slideshow.sync(pb); 1263 } 1264 1265 private static final String SMS_DRAFT_WHERE = Sms.TYPE + "=" + Sms.MESSAGE_TYPE_DRAFT; 1266 private static final String[] SMS_BODY_PROJECTION = { Sms.BODY }; 1267 private static final int SMS_BODY_INDEX = 0; 1268 1269 /** 1270 * Reads a draft message for the given thread ID from the database, 1271 * if there is one, deletes it from the database, and returns it. 1272 * @return The draft message or an empty string. 1273 */ 1274 private String readDraftSmsMessage(Conversation conv) { 1275 long thread_id = conv.getThreadId(); 1276 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 1277 LogTag.debug("readDraftSmsMessage tid=%d", thread_id); 1278 } 1279 // If it's an invalid thread or we know there's no draft, don't bother. 1280 if (thread_id <= 0 || !conv.hasDraft()) { 1281 return ""; 1282 } 1283 1284 Uri thread_uri = ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, thread_id); 1285 String body = ""; 1286 1287 Cursor c = SqliteWrapper.query(mContext, mContentResolver, 1288 thread_uri, SMS_BODY_PROJECTION, SMS_DRAFT_WHERE, null, null); 1289 boolean haveDraft = false; 1290 if (c != null) { 1291 try { 1292 if (c.moveToFirst()) { 1293 body = c.getString(SMS_BODY_INDEX); 1294 haveDraft = true; 1295 } 1296 } finally { 1297 c.close(); 1298 } 1299 } 1300 1301 // We found a draft, and if there are no messages in the conversation, 1302 // that means we deleted the thread, too. Must reset the thread id 1303 // so we'll eventually create a new thread. 1304 if (haveDraft && conv.getMessageCount() == 0) { 1305 // Clean out drafts for this thread -- if the recipient set changes, 1306 // we will lose track of the original draft and be unable to delete 1307 // it later. The message will be re-saved if necessary upon exit of 1308 // the activity. 1309 asyncDeleteDraftSmsMessage(conv); 1310 1311 if (DEBUG) LogTag.debug("readDraftSmsMessage calling clearThreadId"); 1312 conv.clearThreadId(); 1313 1314 // since we removed the draft message in the db, and the conversation no longer 1315 // has a thread id, let's clear the draft state for 'thread_id' in the draft cache. 1316 // Otherwise if a new message arrives it could be assigned the same thread id, and 1317 // we'd mistaken it for a draft due to the stale draft cache. 1318 conv.setDraftState(false); 1319 } 1320 1321 return body; 1322 } 1323 1324 private void asyncUpdateDraftSmsMessage(final Conversation conv, final String contents) { 1325 new Thread(new Runnable() { 1326 public void run() { 1327 long threadId = conv.ensureThreadId(); 1328 conv.setDraftState(true); 1329 updateDraftSmsMessage(threadId, contents); 1330 } 1331 }).start(); 1332 } 1333 1334 private void updateDraftSmsMessage(long thread_id, String contents) { 1335 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 1336 LogTag.debug("updateDraftSmsMessage tid=%d, contents=\"%s\"", thread_id, contents); 1337 } 1338 1339 // If we don't have a valid thread, there's nothing to do. 1340 if (thread_id <= 0) { 1341 return; 1342 } 1343 1344 ContentValues values = new ContentValues(3); 1345 values.put(Sms.THREAD_ID, thread_id); 1346 values.put(Sms.BODY, contents); 1347 values.put(Sms.TYPE, Sms.MESSAGE_TYPE_DRAFT); 1348 SqliteWrapper.insert(mContext, mContentResolver, Sms.CONTENT_URI, values); 1349 asyncDeleteDraftMmsMessage(thread_id); 1350 } 1351 1352 private void asyncDelete(final Uri uri, final String selection, final String[] selectionArgs) { 1353 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 1354 LogTag.debug("asyncDelete %s where %s", uri, selection); 1355 } 1356 new Thread(new Runnable() { 1357 public void run() { 1358 SqliteWrapper.delete(mContext, mContentResolver, uri, selection, selectionArgs); 1359 } 1360 }).start(); 1361 } 1362 1363 private void asyncDeleteDraftSmsMessage(Conversation conv) { 1364 long threadId = conv.getThreadId(); 1365 if (threadId > 0) { 1366 asyncDelete(ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, threadId), 1367 SMS_DRAFT_WHERE, null); 1368 } 1369 } 1370 1371 private void deleteDraftSmsMessage(long threadId) { 1372 SqliteWrapper.delete(mContext, mContentResolver, 1373 ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, threadId), 1374 SMS_DRAFT_WHERE, null); 1375 } 1376 1377 private void asyncDeleteDraftMmsMessage(long threadId) { 1378 final String where = Mms.THREAD_ID + " = " + threadId; 1379 asyncDelete(Mms.Draft.CONTENT_URI, where, null); 1380 } 1381 } 1382