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