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