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