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