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