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