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