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