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