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