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