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