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