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