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