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.io.IOException;
     20 import java.io.InputStream;
     21 import java.util.ArrayList;
     22 import java.util.Arrays;
     23 import java.util.HashMap;
     24 import java.util.Iterator;
     25 import java.util.List;
     26 import java.util.Set;
     27 
     28 import android.app.Activity;
     29 import android.content.ContentResolver;
     30 import android.content.ContentUris;
     31 import android.content.ContentValues;
     32 import android.content.Context;
     33 import android.database.Cursor;
     34 import android.database.sqlite.SqliteWrapper;
     35 import android.net.Uri;
     36 import android.os.AsyncTask;
     37 import android.os.Bundle;
     38 import android.provider.Telephony.Mms;
     39 import android.provider.Telephony.MmsSms;
     40 import android.provider.Telephony.MmsSms.PendingMessages;
     41 import android.provider.Telephony.Sms;
     42 import android.telephony.SmsMessage;
     43 import android.text.TextUtils;
     44 import android.util.Log;
     45 import android.util.Pair;
     46 
     47 import com.android.common.contacts.DataUsageStatUpdater;
     48 import com.android.common.userhappiness.UserHappinessSignals;
     49 import com.android.mms.ContentRestrictionException;
     50 import com.android.mms.ExceedMessageSizeException;
     51 import com.android.mms.LogTag;
     52 import com.android.mms.MmsApp;
     53 import com.android.mms.MmsConfig;
     54 import com.android.mms.ResolutionException;
     55 import com.android.mms.UnsupportContentTypeException;
     56 import com.android.mms.model.ImageModel;
     57 import com.android.mms.model.SlideModel;
     58 import com.android.mms.model.SlideshowModel;
     59 import com.android.mms.model.TextModel;
     60 import com.android.mms.transaction.MessageSender;
     61 import com.android.mms.transaction.MmsMessageSender;
     62 import com.android.mms.transaction.SmsMessageSender;
     63 import com.android.mms.ui.ComposeMessageActivity;
     64 import com.android.mms.ui.MessageUtils;
     65 import com.android.mms.ui.MessagingPreferenceActivity;
     66 import com.android.mms.ui.SlideshowEditor;
     67 import com.android.mms.util.DraftCache;
     68 import com.android.mms.util.Recycler;
     69 import com.android.mms.util.ThumbnailManager;
     70 import com.android.mms.widget.MmsWidgetProvider;
     71 import com.google.android.mms.ContentType;
     72 import com.google.android.mms.MmsException;
     73 import com.google.android.mms.pdu.EncodedStringValue;
     74 import com.google.android.mms.pdu.PduBody;
     75 import com.google.android.mms.pdu.PduHeaders;
     76 import com.google.android.mms.pdu.PduPersister;
     77 import com.google.android.mms.pdu.SendReq;
     78 
     79 /**
     80  * Contains all state related to a message being edited by the user.
     81  */
     82 public class WorkingMessage {
     83     private static final String TAG = "WorkingMessage";
     84     private static final boolean DEBUG = false;
     85 
     86     // Public intents
     87     public static final String ACTION_SENDING_SMS = "android.intent.action.SENDING_SMS";
     88 
     89     // Intent extras
     90     public static final String EXTRA_SMS_MESSAGE = "android.mms.extra.MESSAGE";
     91     public static final String EXTRA_SMS_RECIPIENTS = "android.mms.extra.RECIPIENTS";
     92     public static final String EXTRA_SMS_THREAD_ID = "android.mms.extra.THREAD_ID";
     93 
     94     // Database access stuff
     95     private final Activity mActivity;
     96     private final ContentResolver mContentResolver;
     97 
     98     // States that can require us to save or send a message as MMS.
     99     private static final int RECIPIENTS_REQUIRE_MMS = (1 << 0);     // 1
    100     private static final int HAS_SUBJECT = (1 << 1);                // 2
    101     private static final int HAS_ATTACHMENT = (1 << 2);             // 4
    102     private static final int LENGTH_REQUIRES_MMS = (1 << 3);        // 8
    103     private static final int FORCE_MMS = (1 << 4);                  // 16
    104     private static final int MULTIPLE_RECIPIENTS = (1 << 5);        // 32
    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     // Track whether we have drafts
    144     private volatile boolean mHasMmsDraft;
    145     private volatile boolean mHasSmsDraft;
    146 
    147     // Cached value of mms enabled flag
    148     private static boolean sMmsEnabled = MmsConfig.getMmsEnabled();
    149 
    150     // Our callback interface
    151     private final MessageStatusListener mStatusListener;
    152     private List<String> mWorkingRecipients;
    153 
    154     // Message sizes in Outbox
    155     private static final String[] MMS_OUTBOX_PROJECTION = {
    156         Mms._ID,            // 0
    157         Mms.MESSAGE_SIZE    // 1
    158     };
    159 
    160     private static final int MMS_MESSAGE_SIZE_INDEX  = 1;
    161 
    162     /**
    163      * Callback interface for communicating important state changes back to
    164      * ComposeMessageActivity.
    165      */
    166     public interface MessageStatusListener {
    167         /**
    168          * Called when the protocol for sending the message changes from SMS
    169          * to MMS, and vice versa.
    170          *
    171          * @param mms If true, it changed to MMS.  If false, to SMS.
    172          */
    173         void onProtocolChanged(boolean mms);
    174 
    175         /**
    176          * Called when an attachment on the message has changed.
    177          */
    178         void onAttachmentChanged();
    179 
    180         /**
    181          * Called just before the process of sending a message.
    182          */
    183         void onPreMessageSent();
    184 
    185         /**
    186          * Called once the process of sending a message, triggered by
    187          * {@link send} has completed. This doesn't mean the send succeeded,
    188          * just that it has been dispatched to the network.
    189          */
    190         void onMessageSent();
    191 
    192         /**
    193          * Called if there are too many unsent messages in the queue and we're not allowing
    194          * any more Mms's to be sent.
    195          */
    196         void onMaxPendingMessagesReached();
    197 
    198         /**
    199          * Called if there's an attachment error while resizing the images just before sending.
    200          */
    201         void onAttachmentError(int error);
    202     }
    203 
    204     private WorkingMessage(ComposeMessageActivity activity) {
    205         mActivity = activity;
    206         mContentResolver = mActivity.getContentResolver();
    207         mStatusListener = activity;
    208         mAttachmentType = TEXT;
    209         mText = "";
    210     }
    211 
    212     /**
    213      * Creates a new working message.
    214      */
    215     public static WorkingMessage createEmpty(ComposeMessageActivity activity) {
    216         // Make a new empty working message.
    217         WorkingMessage msg = new WorkingMessage(activity);
    218         return msg;
    219     }
    220 
    221     /**
    222      * Create a new WorkingMessage from the specified data URI, which typically
    223      * contains an MMS message.
    224      */
    225     public static WorkingMessage load(ComposeMessageActivity activity, Uri uri) {
    226         // If the message is not already in the draft box, move it there.
    227         if (!uri.toString().startsWith(Mms.Draft.CONTENT_URI.toString())) {
    228             PduPersister persister = PduPersister.getPduPersister(activity);
    229             if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    230                 LogTag.debug("load: moving %s to drafts", uri);
    231             }
    232             try {
    233                 uri = persister.move(uri, Mms.Draft.CONTENT_URI);
    234             } catch (MmsException e) {
    235                 LogTag.error("Can't move %s to drafts", uri);
    236                 return null;
    237             }
    238         }
    239 
    240         WorkingMessage msg = new WorkingMessage(activity);
    241         if (msg.loadFromUri(uri)) {
    242             msg.mHasMmsDraft = true;
    243             return msg;
    244         }
    245 
    246         return null;
    247     }
    248 
    249     private void correctAttachmentState(boolean showToast) {
    250         int slideCount = mSlideshow.size();
    251 
    252         // If we get an empty slideshow, tear down all MMS
    253         // state and discard the unnecessary message Uri.
    254         if (slideCount == 0) {
    255             removeAttachment(false);
    256         } else if (slideCount > 1) {
    257             mAttachmentType = SLIDESHOW;
    258         } else {
    259             SlideModel slide = mSlideshow.get(0);
    260             if (slide.hasImage()) {
    261                 mAttachmentType = IMAGE;
    262             } else if (slide.hasVideo()) {
    263                 mAttachmentType = VIDEO;
    264             } else if (slide.hasAudio()) {
    265                 mAttachmentType = AUDIO;
    266             }
    267         }
    268 
    269         updateState(HAS_ATTACHMENT, hasAttachment(), showToast);
    270     }
    271 
    272     private boolean loadFromUri(Uri uri) {
    273         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) LogTag.debug("loadFromUri %s", uri);
    274         try {
    275             mSlideshow = SlideshowModel.createFromMessageUri(mActivity, uri);
    276         } catch (MmsException e) {
    277             LogTag.error("Couldn't load URI %s", uri);
    278             return false;
    279         }
    280 
    281         mMessageUri = uri;
    282 
    283         // Make sure all our state is as expected.
    284         syncTextFromSlideshow();
    285         correctAttachmentState(false);
    286 
    287         return true;
    288     }
    289 
    290     /**
    291      * Load the draft message for the specified conversation, or a new empty message if
    292      * none exists.
    293      */
    294     public static WorkingMessage loadDraft(ComposeMessageActivity activity,
    295                                            final Conversation conv,
    296                                            final Runnable onDraftLoaded) {
    297         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) LogTag.debug("loadDraft %s", conv);
    298 
    299         final WorkingMessage msg = createEmpty(activity);
    300         if (conv.getThreadId() <= 0) {
    301             if (onDraftLoaded != null) {
    302                 onDraftLoaded.run();
    303             }
    304             return msg;
    305         }
    306 
    307         new AsyncTask<Void, Void, Pair<String, String>>() {
    308 
    309             // Return a Pair where:
    310             //    first - non-empty String representing the text of an SMS draft
    311             //    second - non-null String representing the text of an MMS subject
    312             @Override
    313             protected Pair<String, String> doInBackground(Void... none) {
    314                 // Look for an SMS draft first.
    315                 String draftText = msg.readDraftSmsMessage(conv);
    316                 String subject = null;
    317 
    318                 if (TextUtils.isEmpty(draftText)) {
    319                     // No SMS draft so look for an MMS draft.
    320                     StringBuilder sb = new StringBuilder();
    321                     Uri uri = readDraftMmsMessage(msg.mActivity, conv, sb);
    322                     if (uri != null) {
    323                         if (msg.loadFromUri(uri)) {
    324                             // If there was an MMS message, readDraftMmsMessage
    325                             // will put the subject in our supplied StringBuilder.
    326                             subject = sb.toString();
    327                         }
    328                     }
    329                 }
    330                 Pair<String, String> result = new Pair<String, String>(draftText, subject);
    331                 return result;
    332             }
    333 
    334             @Override
    335             protected void onPostExecute(Pair<String, String> result) {
    336                 if (!TextUtils.isEmpty(result.first)) {
    337                     msg.mHasSmsDraft = true;
    338                     msg.setText(result.first);
    339                 }
    340                 if (result.second != null) {
    341                     msg.mHasMmsDraft = true;
    342                     if (!TextUtils.isEmpty(result.second)) {
    343                         msg.setSubject(result.second, false);
    344                     }
    345                 }
    346                 if (onDraftLoaded != null) {
    347                     onDraftLoaded.run();
    348                 }
    349             }
    350         }.execute();
    351 
    352         return msg;
    353     }
    354 
    355     /**
    356      * Sets the text of the message to the specified CharSequence.
    357      */
    358     public void setText(CharSequence s) {
    359         mText = s;
    360     }
    361 
    362     /**
    363      * Returns the current message text.
    364      */
    365     public CharSequence getText() {
    366         return mText;
    367     }
    368 
    369     /**
    370      * @return True if the message has any text. A message with just whitespace is not considered
    371      * to have text.
    372      */
    373     public boolean hasText() {
    374         return mText != null && TextUtils.getTrimmedLength(mText) > 0;
    375     }
    376 
    377     public void removeAttachment(boolean notify) {
    378         removeThumbnailsFromCache(mSlideshow);
    379         mAttachmentType = TEXT;
    380         mSlideshow = null;
    381         if (mMessageUri != null) {
    382             asyncDelete(mMessageUri, null, null);
    383             mMessageUri = null;
    384         }
    385         // mark this message as no longer having an attachment
    386         updateState(HAS_ATTACHMENT, false, notify);
    387         if (notify) {
    388             // Tell ComposeMessageActivity (or other listener) that the attachment has changed.
    389             // In the case of ComposeMessageActivity, it will remove its attachment panel because
    390             // this working message no longer has an attachment.
    391             mStatusListener.onAttachmentChanged();
    392         }
    393     }
    394 
    395     public static void removeThumbnailsFromCache(SlideshowModel slideshow) {
    396         if (slideshow != null) {
    397             ThumbnailManager thumbnailManager = MmsApp.getApplication().getThumbnailManager();
    398             boolean removedSomething = false;
    399             Iterator<SlideModel> iterator = slideshow.iterator();
    400             while (iterator.hasNext()) {
    401                 SlideModel slideModel = iterator.next();
    402                 if (slideModel.hasImage()) {
    403                     thumbnailManager.removeThumbnail(slideModel.getImage().getUri());
    404                     removedSomething = true;
    405                 } else if (slideModel.hasVideo()) {
    406                     thumbnailManager.removeThumbnail(slideModel.getVideo().getUri());
    407                     removedSomething = true;
    408                 }
    409             }
    410             if (removedSomething) {
    411                 // HACK: the keys to the thumbnail cache are the part uris, such as mms/part/3
    412                 // Because the part table doesn't have auto-increment ids, the part ids are reused
    413                 // when a message or thread is deleted. For now, we're clearing the whole thumbnail
    414                 // cache so we don't retrieve stale images when part ids are reused. This will be
    415                 // fixed in the next release in the mms provider.
    416                 MmsApp.getApplication().getThumbnailManager().clearBackingStore();
    417             }
    418         }
    419     }
    420 
    421     /**
    422      * Adds an attachment to the message, replacing an old one if it existed.
    423      * @param type Type of this attachment, such as {@link IMAGE}
    424      * @param dataUri Uri containing the attachment data (or null for {@link TEXT})
    425      * @param append true if we should add the attachment to a new slide
    426      * @return An error code such as {@link UNKNOWN_ERROR} or {@link OK} if successful
    427      */
    428     public int setAttachment(int type, Uri dataUri, boolean append) {
    429         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    430             LogTag.debug("setAttachment type=%d uri %s", type, dataUri);
    431         }
    432         int result = OK;
    433         SlideshowEditor slideShowEditor = new SlideshowEditor(mActivity, mSlideshow);
    434 
    435         // Special case for deleting a slideshow. When ComposeMessageActivity gets told to
    436         // remove an attachment (search for AttachmentEditor.MSG_REMOVE_ATTACHMENT), it calls
    437         // this function setAttachment with a type of TEXT and a null uri. Basically, it's turning
    438         // the working message from an MMS back to a simple SMS. The various attachment types
    439         // use slide[0] as a special case. The call to ensureSlideshow below makes sure there's
    440         // a slide zero. In the case of an already attached slideshow, ensureSlideshow will do
    441         // nothing and the slideshow will remain such that if a user adds a slideshow again, they'll
    442         // see their old slideshow they previously deleted. Here we really delete the slideshow.
    443         if (type == TEXT && mAttachmentType == SLIDESHOW && mSlideshow != null && dataUri == null
    444                 && !append) {
    445             slideShowEditor.removeAllSlides();
    446         }
    447 
    448         // Make sure mSlideshow is set up and has a slide.
    449         ensureSlideshow();      // mSlideshow can be null before this call, won't be afterwards
    450         slideShowEditor.setSlideshow(mSlideshow);
    451 
    452         // Change the attachment
    453         result = append ? appendMedia(type, dataUri, slideShowEditor)
    454                 : changeMedia(type, dataUri, slideShowEditor);
    455 
    456         // If we were successful, update mAttachmentType and notify
    457         // the listener than there was a change.
    458         if (result == OK) {
    459             mAttachmentType = type;
    460         }
    461         correctAttachmentState(true);   // this can remove the slideshow if there are no attachments
    462 
    463         if (mSlideshow != null && type == IMAGE) {
    464             // Prime the image's cache; helps A LOT when the image is coming from the network
    465             // (e.g. Picasa album). See b/5445690.
    466             int numSlides = mSlideshow.size();
    467             if (numSlides > 0) {
    468                 ImageModel imgModel = mSlideshow.get(numSlides - 1).getImage();
    469                 if (imgModel != null) {
    470                     cancelThumbnailLoading();
    471                     imgModel.loadThumbnailBitmap(null);
    472                 }
    473             }
    474         }
    475 
    476         mStatusListener.onAttachmentChanged();  // have to call whether succeeded or failed,
    477                                                 // because a replace that fails, removes the slide
    478 
    479         if (!append && mAttachmentType == TEXT && type == TEXT) {
    480             int[] params = SmsMessage.calculateLength(getText(), false);
    481             /* SmsMessage.calculateLength returns an int[4] with:
    482              *   int[0] being the number of SMS's required,
    483              *   int[1] the number of code units used,
    484              *   int[2] is the number of code units remaining until the next message.
    485              *   int[3] is the encoding type that should be used for the message.
    486              */
    487             int smsSegmentCount = params[0];
    488 
    489             if (!MmsConfig.getMultipartSmsEnabled()) {
    490                 // The provider doesn't support multi-part sms's so as soon as the user types
    491                 // an sms longer than one segment, we have to turn the message into an mms.
    492                 setLengthRequiresMms(smsSegmentCount > 1, false);
    493             } else {
    494                 int threshold = MmsConfig.getSmsToMmsTextThreshold();
    495                 setLengthRequiresMms(threshold > 0 && smsSegmentCount > threshold, false);
    496             }
    497         }
    498         return result;
    499     }
    500 
    501     /**
    502      * Returns true if this message contains anything worth saving.
    503      */
    504     public boolean isWorthSaving() {
    505         // If it actually contains anything, it's of course not empty.
    506         if (hasText() || hasSubject() || hasAttachment() || hasSlideshow()) {
    507             return true;
    508         }
    509 
    510         // When saveAsMms() has been called, we set FORCE_MMS to represent
    511         // sort of an "invisible attachment" so that the message isn't thrown
    512         // away when we are shipping it off to other activities.
    513         if (isFakeMmsForDraft()) {
    514             return true;
    515         }
    516 
    517         return false;
    518     }
    519 
    520     private void cancelThumbnailLoading() {
    521         int numSlides = mSlideshow != null ? mSlideshow.size() : 0;
    522         if (numSlides > 0) {
    523             ImageModel imgModel = mSlideshow.get(numSlides - 1).getImage();
    524             if (imgModel != null) {
    525                 imgModel.cancelThumbnailLoading();
    526             }
    527         }
    528     }
    529 
    530     /**
    531      * Returns true if FORCE_MMS is set.
    532      * When saveAsMms() has been called, we set FORCE_MMS to represent
    533      * sort of an "invisible attachment" so that the message isn't thrown
    534      * away when we are shipping it off to other activities.
    535      */
    536     public boolean isFakeMmsForDraft() {
    537         return (mMmsState & FORCE_MMS) > 0;
    538     }
    539 
    540     /**
    541      * Makes sure mSlideshow is set up.
    542      */
    543     private void ensureSlideshow() {
    544         if (mSlideshow != null) {
    545             return;
    546         }
    547 
    548         SlideshowModel slideshow = SlideshowModel.createNew(mActivity);
    549         SlideModel slide = new SlideModel(slideshow);
    550         slideshow.add(slide);
    551 
    552         mSlideshow = slideshow;
    553     }
    554 
    555     /**
    556      * Change the message's attachment to the data in the specified Uri.
    557      * Used only for single-slide ("attachment mode") messages. If the attachment fails to
    558      * attach, restore the slide to its original state.
    559      */
    560     private int changeMedia(int type, Uri uri, SlideshowEditor slideShowEditor) {
    561         SlideModel originalSlide = mSlideshow.get(0);
    562         if (originalSlide != null) {
    563             slideShowEditor.removeSlide(0);     // remove the original slide
    564         }
    565         slideShowEditor.addNewSlide(0);
    566         SlideModel slide = mSlideshow.get(0);   // get the new empty slide
    567         int result = OK;
    568 
    569         if (slide == null) {
    570             Log.w(LogTag.TAG, "[WorkingMessage] changeMedia: no slides!");
    571             return result;
    572         }
    573 
    574         // Clear the attachment type since we removed all the attachments. If this isn't cleared
    575         // and the slide.add fails (for instance, a selected video could be too big), we'll be
    576         // left in a state where we think we have an attachment, but it's been removed from the
    577         // slide.
    578         mAttachmentType = TEXT;
    579 
    580         // If we're changing to text, just bail out.
    581         if (type == TEXT) {
    582             return result;
    583         }
    584 
    585         result = internalChangeMedia(type, uri, 0, slideShowEditor);
    586         if (result != OK) {
    587             slideShowEditor.removeSlide(0);             // remove the failed slide
    588             if (originalSlide != null) {
    589                 slideShowEditor.addSlide(0, originalSlide); // restore the original slide.
    590             }
    591         }
    592         return result;
    593     }
    594 
    595     /**
    596      * Add the message's attachment to the data in the specified Uri to a new slide.
    597      */
    598     private int appendMedia(int type, Uri uri, SlideshowEditor slideShowEditor) {
    599         int result = OK;
    600 
    601         // If we're changing to text, just bail out.
    602         if (type == TEXT) {
    603             return result;
    604         }
    605 
    606         // The first time this method is called, mSlideshow.size() is going to be
    607         // one (a newly initialized slideshow has one empty slide). The first time we
    608         // attach the picture/video to that first empty slide. From then on when this
    609         // function is called, we've got to create a new slide and add the picture/video
    610         // to that new slide.
    611         boolean addNewSlide = true;
    612         if (mSlideshow.size() == 1 && !mSlideshow.isSimple()) {
    613             addNewSlide = false;
    614         }
    615         if (addNewSlide) {
    616             if (!slideShowEditor.addNewSlide()) {
    617                 return result;
    618             }
    619         }
    620         int slideNum = mSlideshow.size() - 1;
    621         result = internalChangeMedia(type, uri, slideNum, slideShowEditor);
    622         if (result != OK) {
    623             // We added a new slide and what we attempted to insert on the slide failed.
    624             // Delete that slide, otherwise we could end up with a bunch of blank slides.
    625             // It's ok that we're removing the slide even if we didn't add it (because it was
    626             // the first default slide). If adding the first slide fails, we want to remove it.
    627             slideShowEditor.removeSlide(slideNum);
    628         }
    629         return result;
    630     }
    631 
    632     private int internalChangeMedia(int type, Uri uri, int slideNum,
    633             SlideshowEditor slideShowEditor) {
    634         int result = OK;
    635         try {
    636             if (type == IMAGE) {
    637                 slideShowEditor.changeImage(slideNum, uri);
    638             } else if (type == VIDEO) {
    639                 slideShowEditor.changeVideo(slideNum, uri);
    640             } else if (type == AUDIO) {
    641                 slideShowEditor.changeAudio(slideNum, uri);
    642             } else {
    643                 result = UNSUPPORTED_TYPE;
    644             }
    645         } catch (MmsException e) {
    646             Log.e(TAG, "internalChangeMedia:", e);
    647             result = UNKNOWN_ERROR;
    648         } catch (UnsupportContentTypeException e) {
    649             Log.e(TAG, "internalChangeMedia:", e);
    650             result = UNSUPPORTED_TYPE;
    651         } catch (ExceedMessageSizeException e) {
    652             Log.e(TAG, "internalChangeMedia:", e);
    653             result = MESSAGE_SIZE_EXCEEDED;
    654         } catch (ResolutionException e) {
    655             Log.e(TAG, "internalChangeMedia:", e);
    656             result = IMAGE_TOO_LARGE;
    657         }
    658         return result;
    659     }
    660 
    661     /**
    662      * Returns true if the message has an attachment (including slideshows).
    663      */
    664     public boolean hasAttachment() {
    665         return (mAttachmentType > TEXT);
    666     }
    667 
    668     /**
    669      * Returns the slideshow associated with this message.
    670      */
    671     public SlideshowModel getSlideshow() {
    672         return mSlideshow;
    673     }
    674 
    675     /**
    676      * Returns true if the message has a real slideshow, as opposed to just
    677      * one image attachment, for example.
    678      */
    679     public boolean hasSlideshow() {
    680         return (mAttachmentType == SLIDESHOW);
    681     }
    682 
    683     /**
    684      * Sets the MMS subject of the message.  Passing null indicates that there
    685      * is no subject.  Passing "" will result in an empty subject being added
    686      * to the message, possibly triggering a conversion to MMS.  This extra
    687      * bit of state is needed to support ComposeMessageActivity converting to
    688      * MMS when the user adds a subject.  An empty subject will be removed
    689      * before saving to disk or sending, however.
    690      */
    691     public void setSubject(CharSequence s, boolean notify) {
    692         mSubject = s;
    693         updateState(HAS_SUBJECT, (s != null), notify);
    694     }
    695 
    696     /**
    697      * Returns the MMS subject of the message.
    698      */
    699     public CharSequence getSubject() {
    700         return mSubject;
    701     }
    702 
    703     /**
    704      * Returns true if this message has an MMS subject. A subject has to be more than just
    705      * whitespace.
    706      * @return
    707      */
    708     public boolean hasSubject() {
    709         return mSubject != null && TextUtils.getTrimmedLength(mSubject) > 0;
    710     }
    711 
    712     /**
    713      * Moves the message text into the slideshow.  Should be called any time
    714      * the message is about to be sent or written to disk.
    715      */
    716     private void syncTextToSlideshow() {
    717         if (mSlideshow == null || mSlideshow.size() != 1)
    718             return;
    719 
    720         SlideModel slide = mSlideshow.get(0);
    721         TextModel text;
    722         if (!slide.hasText()) {
    723             // Add a TextModel to slide 0 if one doesn't already exist
    724             text = new TextModel(mActivity, ContentType.TEXT_PLAIN, "text_0.txt",
    725                                            mSlideshow.getLayout().getTextRegion());
    726             slide.add(text);
    727         } else {
    728             // Otherwise just reuse the existing one.
    729             text = slide.getText();
    730         }
    731         text.setText(mText);
    732     }
    733 
    734     /**
    735      * Sets the message text out of the slideshow.  Should be called any time
    736      * a slideshow is loaded from disk.
    737      */
    738     private void syncTextFromSlideshow() {
    739         // Don't sync text for real slideshows.
    740         if (mSlideshow.size() != 1) {
    741             return;
    742         }
    743 
    744         SlideModel slide = mSlideshow.get(0);
    745         if (slide == null || !slide.hasText()) {
    746             return;
    747         }
    748 
    749         mText = slide.getText().getText();
    750     }
    751 
    752     /**
    753      * Removes the subject if it is empty, possibly converting back to SMS.
    754      */
    755     private void removeSubjectIfEmpty(boolean notify) {
    756         if (!hasSubject()) {
    757             setSubject(null, notify);
    758         }
    759     }
    760 
    761     /**
    762      * Gets internal message state ready for storage.  Should be called any
    763      * time the message is about to be sent or written to disk.
    764      */
    765     private void prepareForSave(boolean notify) {
    766         // Make sure our working set of recipients is resolved
    767         // to first-class Contact objects before we save.
    768         syncWorkingRecipients();
    769 
    770         if (hasMmsContentToSave()) {
    771             ensureSlideshow();
    772             syncTextToSlideshow();
    773         }
    774     }
    775 
    776     /**
    777      * Resolve the temporary working set of recipients to a ContactList.
    778      */
    779     public void syncWorkingRecipients() {
    780         if (mWorkingRecipients != null) {
    781             ContactList recipients = ContactList.getByNumbers(mWorkingRecipients, false);
    782             mConversation.setRecipients(recipients);    // resets the threadId to zero
    783             setHasMultipleRecipients(recipients.size() > 1, true);
    784             mWorkingRecipients = null;
    785         }
    786     }
    787 
    788     public String getWorkingRecipients() {
    789         // this function is used for DEBUG only
    790         if (mWorkingRecipients == null) {
    791             return null;
    792         }
    793         ContactList recipients = ContactList.getByNumbers(mWorkingRecipients, false);
    794         return recipients.serialize();
    795     }
    796 
    797     // Call when we've returned from adding an attachment. We're no longer forcing the message
    798     // into a Mms message. At this point we either have the goods to make the message a Mms
    799     // or we don't. No longer fake it.
    800     public void removeFakeMmsForDraft() {
    801         updateState(FORCE_MMS, false, false);
    802     }
    803 
    804     /**
    805      * Force the message to be saved as MMS and return the Uri of the message.
    806      * Typically used when handing a message off to another activity.
    807      */
    808     public Uri saveAsMms(boolean notify) {
    809         if (DEBUG) LogTag.debug("saveAsMms mConversation=%s", mConversation);
    810 
    811         // If we have discarded the message, just bail out.
    812         if (mDiscarded) {
    813             LogTag.warn("saveAsMms mDiscarded: true mConversation: " + mConversation +
    814                     " returning NULL uri and bailing");
    815             return null;
    816         }
    817 
    818         // FORCE_MMS behaves as sort of an "invisible attachment", making
    819         // the message seem non-empty (and thus not discarded).  This bit
    820         // is sticky until the last other MMS bit is removed, at which
    821         // point the message will fall back to SMS.
    822         updateState(FORCE_MMS, true, notify);
    823 
    824         // Collect our state to be written to disk.
    825         prepareForSave(true /* notify */);
    826 
    827         try {
    828             // Make sure we are saving to the correct thread ID.
    829             DraftCache.getInstance().setSavingDraft(true);
    830             if (!mConversation.getRecipients().isEmpty()) {
    831                 mConversation.ensureThreadId();
    832             }
    833             mConversation.setDraftState(true);
    834 
    835             PduPersister persister = PduPersister.getPduPersister(mActivity);
    836             SendReq sendReq = makeSendReq(mConversation, mSubject);
    837 
    838             // If we don't already have a Uri lying around, make a new one.  If we do
    839             // have one already, make sure it is synced to disk.
    840             if (mMessageUri == null) {
    841                 mMessageUri = createDraftMmsMessage(persister, sendReq, mSlideshow, null,
    842                         mActivity, null);
    843             } else {
    844                 updateDraftMmsMessage(mMessageUri, persister, mSlideshow, sendReq, null);
    845             }
    846             mHasMmsDraft = true;
    847         } finally {
    848             DraftCache.getInstance().setSavingDraft(false);
    849         }
    850         return mMessageUri;
    851     }
    852 
    853     /**
    854      * Save this message as a draft in the conversation previously specified
    855      * to {@link setConversation}.
    856      */
    857     public void saveDraft(final boolean isStopping) {
    858         // If we have discarded the message, just bail out.
    859         if (mDiscarded) {
    860             LogTag.warn("saveDraft mDiscarded: true mConversation: " + mConversation +
    861                 " skipping saving draft and bailing");
    862             return;
    863         }
    864 
    865         // Make sure setConversation was called.
    866         if (mConversation == null) {
    867             throw new IllegalStateException("saveDraft() called with no conversation");
    868         }
    869 
    870         if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    871             LogTag.debug("saveDraft for mConversation " + mConversation);
    872         }
    873 
    874         // Get ready to write to disk. But don't notify message status when saving draft
    875         prepareForSave(false /* notify */);
    876 
    877         if (requiresMms()) {
    878             if (hasMmsContentToSave()) {
    879                 asyncUpdateDraftMmsMessage(mConversation, isStopping);
    880                 mHasMmsDraft = true;
    881             }
    882         } else {
    883             String content = mText.toString();
    884 
    885             // bug 2169583: don't bother creating a thread id only to delete the thread
    886             // because the content is empty. When we delete the thread in updateDraftSmsMessage,
    887             // we didn't nullify conv.mThreadId, causing a temperary situation where conv
    888             // is holding onto a thread id that isn't in the database. If a new message arrives
    889             // and takes that thread id (because it's the next thread id to be assigned), the
    890             // new message will be merged with the draft message thread, causing confusion!
    891             if (!TextUtils.isEmpty(content)) {
    892                 asyncUpdateDraftSmsMessage(mConversation, content, isStopping);
    893                 mHasSmsDraft = true;
    894             } else {
    895                 // When there's no associated text message, we have to handle the case where there
    896                 // might have been a previous mms draft for this message. This can happen when a
    897                 // user turns an mms back into a sms, such as creating an mms draft with a picture,
    898                 // then removing the picture.
    899                 asyncDeleteDraftMmsMessage(mConversation);
    900                 mMessageUri = null;
    901             }
    902         }
    903     }
    904 
    905     synchronized public void discard() {
    906         if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    907             LogTag.debug("[WorkingMessage] discard");
    908         }
    909 
    910         if (mDiscarded == true) {
    911             return;
    912         }
    913 
    914         // Mark this message as discarded in order to make saveDraft() no-op.
    915         mDiscarded = true;
    916 
    917         cancelThumbnailLoading();
    918 
    919         // Delete any associated drafts if there are any.
    920         if (mHasMmsDraft) {
    921             asyncDeleteDraftMmsMessage(mConversation);
    922         }
    923         if (mHasSmsDraft) {
    924             asyncDeleteDraftSmsMessage(mConversation);
    925         }
    926         clearConversation(mConversation, true);
    927     }
    928 
    929     public void unDiscard() {
    930         if (DEBUG) LogTag.debug("unDiscard");
    931 
    932         mDiscarded = false;
    933     }
    934 
    935     /**
    936      * Returns true if discard() has been called on this message.
    937      */
    938     public boolean isDiscarded() {
    939         return mDiscarded;
    940     }
    941 
    942     /**
    943      * To be called from our Activity's onSaveInstanceState() to give us a chance
    944      * to stow our state away for later retrieval.
    945      *
    946      * @param bundle The Bundle passed in to onSaveInstanceState
    947      */
    948     public void writeStateToBundle(Bundle bundle) {
    949         if (hasSubject()) {
    950             bundle.putString("subject", mSubject.toString());
    951         }
    952 
    953         if (mMessageUri != null) {
    954             bundle.putParcelable("msg_uri", mMessageUri);
    955         } else if (hasText()) {
    956             bundle.putString("sms_body", mText.toString());
    957         }
    958     }
    959 
    960     /**
    961      * To be called from our Activity's onCreate() if the activity manager
    962      * has given it a Bundle to reinflate
    963      * @param bundle The Bundle passed in to onCreate
    964      */
    965     public void readStateFromBundle(Bundle bundle) {
    966         if (bundle == null) {
    967             return;
    968         }
    969 
    970         String subject = bundle.getString("subject");
    971         setSubject(subject, false);
    972 
    973         Uri uri = (Uri)bundle.getParcelable("msg_uri");
    974         if (uri != null) {
    975             loadFromUri(uri);
    976             return;
    977         } else {
    978             String body = bundle.getString("sms_body");
    979             mText = body;
    980         }
    981     }
    982 
    983     /**
    984      * Update the temporary list of recipients, used when setting up a
    985      * new conversation.  Will be converted to a ContactList on any
    986      * save event (send, save draft, etc.)
    987      */
    988     public void setWorkingRecipients(List<String> numbers) {
    989         mWorkingRecipients = numbers;
    990         String s = null;
    991         if (numbers != null) {
    992             int size = numbers.size();
    993             switch (size) {
    994             case 1:
    995                 s = numbers.get(0);
    996                 break;
    997             case 0:
    998                 s = "empty";
    999                 break;
   1000             default:
   1001                 s = "{...} len=" + size;
   1002             }
   1003         }
   1004     }
   1005 
   1006     private void dumpWorkingRecipients() {
   1007         Log.i(TAG, "-- mWorkingRecipients:");
   1008 
   1009         if (mWorkingRecipients != null) {
   1010             int count = mWorkingRecipients.size();
   1011             for (int i=0; i<count; i++) {
   1012                 Log.i(TAG, "   [" + i + "] " + mWorkingRecipients.get(i));
   1013             }
   1014             Log.i(TAG, "");
   1015         }
   1016     }
   1017 
   1018     public void dump() {
   1019         Log.i(TAG, "WorkingMessage:");
   1020         dumpWorkingRecipients();
   1021         if (mConversation != null) {
   1022             Log.i(TAG, "mConversation: " + mConversation.toString());
   1023         }
   1024     }
   1025 
   1026     /**
   1027      * Set the conversation associated with this message.
   1028      */
   1029     public void setConversation(Conversation conv) {
   1030         if (DEBUG) LogTag.debug("setConversation %s -> %s", mConversation, conv);
   1031 
   1032         mConversation = conv;
   1033 
   1034         // Convert to MMS if there are any email addresses in the recipient list.
   1035         ContactList contactList = conv.getRecipients();
   1036         setHasEmail(contactList.containsEmail(), false);
   1037         setHasMultipleRecipients(contactList.size() > 1, false);
   1038     }
   1039 
   1040     public Conversation getConversation() {
   1041         return mConversation;
   1042     }
   1043 
   1044     /**
   1045      * Hint whether or not this message will be delivered to an
   1046      * an email address.
   1047      */
   1048     public void setHasEmail(boolean hasEmail, boolean notify) {
   1049         if (MmsConfig.getEmailGateway() != null) {
   1050             updateState(RECIPIENTS_REQUIRE_MMS, false, notify);
   1051         } else {
   1052             updateState(RECIPIENTS_REQUIRE_MMS, hasEmail, notify);
   1053         }
   1054     }
   1055     /**
   1056      * Set whether this message will be sent to multiple recipients. This is a hint whether the
   1057      * message needs to be sent as an mms or not. If MmsConfig.getGroupMmsEnabled is false, then
   1058      * the fact that the message is sent to multiple recipients is not a factor in determining
   1059      * whether the message is sent as an mms, but the other factors (such as, "has a picture
   1060      * attachment") still hold true.
   1061      */
   1062     public void setHasMultipleRecipients(boolean hasMultipleRecipients, boolean notify) {
   1063         updateState(MULTIPLE_RECIPIENTS,
   1064                 hasMultipleRecipients &&
   1065                     MessagingPreferenceActivity.getIsGroupMmsEnabled(mActivity),
   1066                 notify);
   1067     }
   1068 
   1069     /**
   1070      * Returns true if this message would require MMS to send.
   1071      */
   1072     public boolean requiresMms() {
   1073         return (mMmsState > 0);
   1074     }
   1075 
   1076     /**
   1077      * Returns true if this message has been turned into an mms because it has a subject or
   1078      * an attachment, but not just because it has multiple recipients.
   1079      */
   1080     private boolean hasMmsContentToSave() {
   1081         if (mMmsState == 0) {
   1082             return false;
   1083         }
   1084         if (mMmsState == MULTIPLE_RECIPIENTS && !hasText()) {
   1085             // If this message is only mms because of multiple recipients and there's no text
   1086             // to save, don't bother saving.
   1087             return false;
   1088         }
   1089         return true;
   1090     }
   1091 
   1092     /**
   1093      * Set whether or not we want to send this message via MMS in order to
   1094      * avoid sending an excessive number of concatenated SMS messages.
   1095      * @param: mmsRequired is the value for the LENGTH_REQUIRES_MMS bit.
   1096      * @param: notify Whether or not to notify the user.
   1097     */
   1098     public void setLengthRequiresMms(boolean mmsRequired, boolean notify) {
   1099         updateState(LENGTH_REQUIRES_MMS, mmsRequired, notify);
   1100     }
   1101 
   1102     private static String stateString(int state) {
   1103         if (state == 0)
   1104             return "<none>";
   1105 
   1106         StringBuilder sb = new StringBuilder();
   1107         if ((state & RECIPIENTS_REQUIRE_MMS) > 0)
   1108             sb.append("RECIPIENTS_REQUIRE_MMS | ");
   1109         if ((state & HAS_SUBJECT) > 0)
   1110             sb.append("HAS_SUBJECT | ");
   1111         if ((state & HAS_ATTACHMENT) > 0)
   1112             sb.append("HAS_ATTACHMENT | ");
   1113         if ((state & LENGTH_REQUIRES_MMS) > 0)
   1114             sb.append("LENGTH_REQUIRES_MMS | ");
   1115         if ((state & FORCE_MMS) > 0)
   1116             sb.append("FORCE_MMS | ");
   1117         if ((state & MULTIPLE_RECIPIENTS) > 0)
   1118             sb.append("MULTIPLE_RECIPIENTS | ");
   1119 
   1120         sb.delete(sb.length() - 3, sb.length());
   1121         return sb.toString();
   1122     }
   1123 
   1124     /**
   1125      * Sets the current state of our various "MMS required" bits.
   1126      *
   1127      * @param state The bit to change, such as {@link HAS_ATTACHMENT}
   1128      * @param on If true, set it; if false, clear it
   1129      * @param notify Whether or not to notify the user
   1130      */
   1131     private void updateState(int state, boolean on, boolean notify) {
   1132         if (!sMmsEnabled) {
   1133             // If Mms isn't enabled, the rest of the Messaging UI should not be using any
   1134             // feature that would cause us to to turn on any Mms flag and show the
   1135             // "Converting to multimedia..." message.
   1136             return;
   1137         }
   1138         int oldState = mMmsState;
   1139         if (on) {
   1140             mMmsState |= state;
   1141         } else {
   1142             mMmsState &= ~state;
   1143         }
   1144 
   1145         // If we are clearing the last bit that is not FORCE_MMS,
   1146         // expire the FORCE_MMS bit.
   1147         if (mMmsState == FORCE_MMS && ((oldState & ~FORCE_MMS) > 0)) {
   1148             mMmsState = 0;
   1149         }
   1150 
   1151         // Notify the listener if we are moving from SMS to MMS
   1152         // or vice versa.
   1153         if (notify) {
   1154             if (oldState == 0 && mMmsState != 0) {
   1155                 mStatusListener.onProtocolChanged(true);
   1156             } else if (oldState != 0 && mMmsState == 0) {
   1157                 mStatusListener.onProtocolChanged(false);
   1158             }
   1159         }
   1160 
   1161         if (oldState != mMmsState) {
   1162             if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) LogTag.debug("updateState: %s%s = %s",
   1163                     on ? "+" : "-",
   1164                     stateString(state), stateString(mMmsState));
   1165         }
   1166     }
   1167 
   1168     /**
   1169      * Send this message over the network.  Will call back with onMessageSent() once
   1170      * it has been dispatched to the telephony stack.  This WorkingMessage object is
   1171      * no longer useful after this method has been called.
   1172      *
   1173      * @throws ContentRestrictionException if sending an MMS and uaProfUrl is not defined
   1174      * in mms_config.xml.
   1175      */
   1176     public void send(final String recipientsInUI) {
   1177         long origThreadId = mConversation.getThreadId();
   1178 
   1179         if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
   1180             LogTag.debug("send origThreadId: " + origThreadId);
   1181         }
   1182 
   1183         removeSubjectIfEmpty(true /* notify */);
   1184 
   1185         // Get ready to write to disk.
   1186         prepareForSave(true /* notify */);
   1187 
   1188         // We need the recipient list for both SMS and MMS.
   1189         final Conversation conv = mConversation;
   1190         String msgTxt = mText.toString();
   1191 
   1192         if (requiresMms() || addressContainsEmailToMms(conv, msgTxt)) {
   1193             // uaProfUrl setting in mms_config.xml must be present to send an MMS.
   1194             // However, SMS service will still work in the absence of a uaProfUrl address.
   1195             if (MmsConfig.getUaProfUrl() == null) {
   1196                 String err = "WorkingMessage.send MMS sending failure. mms_config.xml is " +
   1197                         "missing uaProfUrl setting.  uaProfUrl is required for MMS service, " +
   1198                         "but can be absent for SMS.";
   1199                 RuntimeException ex = new NullPointerException(err);
   1200                 Log.e(TAG, err, ex);
   1201                 // now, let's just crash.
   1202                 throw ex;
   1203             }
   1204 
   1205             // Make local copies of the bits we need for sending a message,
   1206             // because we will be doing it off of the main thread, which will
   1207             // immediately continue on to resetting some of this state.
   1208             final Uri mmsUri = mMessageUri;
   1209             final PduPersister persister = PduPersister.getPduPersister(mActivity);
   1210 
   1211             final SlideshowModel slideshow = mSlideshow;
   1212             final CharSequence subject = mSubject;
   1213             final boolean textOnly = mAttachmentType == TEXT;
   1214 
   1215             if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
   1216                 LogTag.debug("Send mmsUri: " + mmsUri);
   1217             }
   1218 
   1219             // Do the dirty work of sending the message off of the main UI thread.
   1220             new Thread(new Runnable() {
   1221                 @Override
   1222                 public void run() {
   1223                     final SendReq sendReq = makeSendReq(conv, subject);
   1224 
   1225                     // Make sure the text in slide 0 is no longer holding onto a reference to
   1226                     // the text in the message text box.
   1227                     slideshow.prepareForSend();
   1228                     sendMmsWorker(conv, mmsUri, persister, slideshow, sendReq, textOnly);
   1229 
   1230                     updateSendStats(conv);
   1231                 }
   1232             }, "WorkingMessage.send MMS").start();
   1233         } else {
   1234             // Same rules apply as above.
   1235             final String msgText = mText.toString();
   1236             new Thread(new Runnable() {
   1237                 @Override
   1238                 public void run() {
   1239                     preSendSmsWorker(conv, msgText, recipientsInUI);
   1240 
   1241                     updateSendStats(conv);
   1242                 }
   1243             }, "WorkingMessage.send SMS").start();
   1244         }
   1245 
   1246         // update the Recipient cache with the new to address, if it's different
   1247         RecipientIdCache.updateNumbers(conv.getThreadId(), conv.getRecipients());
   1248 
   1249         // Mark the message as discarded because it is "off the market" after being sent.
   1250         mDiscarded = true;
   1251     }
   1252 
   1253     // Be sure to only call this on a background thread.
   1254     private void updateSendStats(final Conversation conv) {
   1255         String[] dests = conv.getRecipients().getNumbers();
   1256         final ArrayList<String> phoneNumbers = new ArrayList<String>(Arrays.asList(dests));
   1257 
   1258         DataUsageStatUpdater updater = new DataUsageStatUpdater(mActivity);
   1259         updater.updateWithPhoneNumber(phoneNumbers);
   1260     }
   1261 
   1262     private boolean addressContainsEmailToMms(Conversation conv, String text) {
   1263         if (MmsConfig.getEmailGateway() != null) {
   1264             String[] dests = conv.getRecipients().getNumbers();
   1265             int length = dests.length;
   1266             for (int i = 0; i < length; i++) {
   1267                 if (Mms.isEmailAddress(dests[i]) || MessageUtils.isAlias(dests[i])) {
   1268                     String mtext = dests[i] + " " + text;
   1269                     int[] params = SmsMessage.calculateLength(mtext, false);
   1270                     if (params[0] > 1) {
   1271                         updateState(RECIPIENTS_REQUIRE_MMS, true, true);
   1272                         ensureSlideshow();
   1273                         syncTextToSlideshow();
   1274                         return true;
   1275                     }
   1276                 }
   1277             }
   1278         }
   1279         return false;
   1280     }
   1281 
   1282     // Message sending stuff
   1283 
   1284     private void preSendSmsWorker(Conversation conv, String msgText, String recipientsInUI) {
   1285         // If user tries to send the message, it's a signal the inputted text is what they wanted.
   1286         UserHappinessSignals.userAcceptedImeText(mActivity);
   1287 
   1288         mStatusListener.onPreMessageSent();
   1289 
   1290         long origThreadId = conv.getThreadId();
   1291 
   1292         // Make sure we are still using the correct thread ID for our recipient set.
   1293         long threadId = conv.ensureThreadId();
   1294 
   1295         String semiSepRecipients = conv.getRecipients().serialize();
   1296 
   1297         // recipientsInUI can be empty when the user types in a number and hits send
   1298         if (LogTag.SEVERE_WARNING && ((origThreadId != 0 && origThreadId != threadId) ||
   1299                (!semiSepRecipients.equals(recipientsInUI) && !TextUtils.isEmpty(recipientsInUI)))) {
   1300             String msg = origThreadId != 0 && origThreadId != threadId ?
   1301                     "WorkingMessage.preSendSmsWorker threadId changed or " +
   1302                     "recipients changed. origThreadId: " +
   1303                     origThreadId + " new threadId: " + threadId +
   1304                     " also mConversation.getThreadId(): " +
   1305                     mConversation.getThreadId()
   1306                 :
   1307                     "Recipients in window: \"" +
   1308                     recipientsInUI + "\" differ from recipients from conv: \"" +
   1309                     semiSepRecipients + "\"";
   1310 
   1311             // Just interrupt the process of sending message if recipient mismatch
   1312             LogTag.warnPossibleRecipientMismatch(msg, mActivity);
   1313         }else {
   1314             // just do a regular send. We're already on a non-ui thread so no need to fire
   1315             // off another thread to do this work.
   1316             sendSmsWorker(msgText, semiSepRecipients, threadId);
   1317 
   1318             // Be paranoid and clean any draft SMS up.
   1319             deleteDraftSmsMessage(threadId);
   1320         }
   1321     }
   1322 
   1323     private void sendSmsWorker(String msgText, String semiSepRecipients, long threadId) {
   1324         String[] dests = TextUtils.split(semiSepRecipients, ";");
   1325         if (LogTag.VERBOSE || Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
   1326             Log.d(LogTag.TRANSACTION, "sendSmsWorker sending message: recipients=" +
   1327                     semiSepRecipients + ", threadId=" + threadId);
   1328         }
   1329         MessageSender sender = new SmsMessageSender(mActivity, dests, msgText, threadId);
   1330         try {
   1331             sender.sendMessage(threadId);
   1332 
   1333             // Make sure this thread isn't over the limits in message count
   1334             Recycler.getSmsRecycler().deleteOldMessagesByThreadId(mActivity, threadId);
   1335         } catch (Exception e) {
   1336             Log.e(TAG, "Failed to send SMS message, threadId=" + threadId, e);
   1337         }
   1338 
   1339         mStatusListener.onMessageSent();
   1340         MmsWidgetProvider.notifyDatasetChanged(mActivity);
   1341     }
   1342 
   1343     private void sendMmsWorker(Conversation conv, Uri mmsUri, PduPersister persister,
   1344             SlideshowModel slideshow, SendReq sendReq, boolean textOnly) {
   1345         long threadId = 0;
   1346         Cursor cursor = null;
   1347         boolean newMessage = false;
   1348         try {
   1349             // Put a placeholder message in the database first
   1350             DraftCache.getInstance().setSavingDraft(true);
   1351             mStatusListener.onPreMessageSent();
   1352 
   1353             // Make sure we are still using the correct thread ID for our
   1354             // recipient set.
   1355             threadId = conv.ensureThreadId();
   1356 
   1357             if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1358                 LogTag.debug("sendMmsWorker: update draft MMS message " + mmsUri +
   1359                         " threadId: " + threadId);
   1360             }
   1361 
   1362             // One last check to verify the address of the recipient.
   1363             String[] dests = conv.getRecipients().getNumbers(true /* scrub for MMS address */);
   1364             if (dests.length == 1) {
   1365                 // verify the single address matches what's in the database. If we get a different
   1366                 // address back, jam the new value back into the SendReq.
   1367                 String newAddress =
   1368                     Conversation.verifySingleRecipient(mActivity, conv.getThreadId(), dests[0]);
   1369 
   1370                 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1371                     LogTag.debug("sendMmsWorker: newAddress " + newAddress +
   1372                             " dests[0]: " + dests[0]);
   1373                 }
   1374 
   1375                 if (!newAddress.equals(dests[0])) {
   1376                     dests[0] = newAddress;
   1377                     EncodedStringValue[] encodedNumbers = EncodedStringValue.encodeStrings(dests);
   1378                     if (encodedNumbers != null) {
   1379                         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1380                             LogTag.debug("sendMmsWorker: REPLACING number!!!");
   1381                         }
   1382                         sendReq.setTo(encodedNumbers);
   1383                     }
   1384                 }
   1385             }
   1386             newMessage = mmsUri == null;
   1387             if (newMessage) {
   1388                 // Write something in the database so the new message will appear as sending
   1389                 ContentValues values = new ContentValues();
   1390                 values.put(Mms.MESSAGE_BOX, Mms.MESSAGE_BOX_OUTBOX);
   1391                 values.put(Mms.THREAD_ID, threadId);
   1392                 values.put(Mms.MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_SEND_REQ);
   1393                 if (textOnly) {
   1394                     values.put(Mms.TEXT_ONLY, 1);
   1395                 }
   1396                 mmsUri = SqliteWrapper.insert(mActivity, mContentResolver, Mms.Outbox.CONTENT_URI,
   1397                         values);
   1398             }
   1399             mStatusListener.onMessageSent();
   1400 
   1401             // If user tries to send the message, it's a signal the inputted text is
   1402             // what they wanted.
   1403             UserHappinessSignals.userAcceptedImeText(mActivity);
   1404 
   1405             // First make sure we don't have too many outstanding unsent message.
   1406             cursor = SqliteWrapper.query(mActivity, mContentResolver,
   1407                     Mms.Outbox.CONTENT_URI, MMS_OUTBOX_PROJECTION, null, null, null);
   1408             if (cursor != null) {
   1409                 long maxMessageSize = MmsConfig.getMaxSizeScaleForPendingMmsAllowed() *
   1410                 MmsConfig.getMaxMessageSize();
   1411                 long totalPendingSize = 0;
   1412                 while (cursor.moveToNext()) {
   1413                     totalPendingSize += cursor.getLong(MMS_MESSAGE_SIZE_INDEX);
   1414                 }
   1415                 if (totalPendingSize >= maxMessageSize) {
   1416                     unDiscard();    // it wasn't successfully sent. Allow it to be saved as a draft.
   1417                     mStatusListener.onMaxPendingMessagesReached();
   1418                     markMmsMessageWithError(mmsUri);
   1419                     return;
   1420                 }
   1421             }
   1422         } finally {
   1423             if (cursor != null) {
   1424                 cursor.close();
   1425             }
   1426         }
   1427 
   1428         try {
   1429             if (newMessage) {
   1430                 // Create a new MMS message if one hasn't been made yet.
   1431                 mmsUri = createDraftMmsMessage(persister, sendReq, slideshow, mmsUri,
   1432                         mActivity, null);
   1433             } else {
   1434                 // Otherwise, sync the MMS message in progress to disk.
   1435                 updateDraftMmsMessage(mmsUri, persister, slideshow, sendReq, null);
   1436             }
   1437 
   1438             // Be paranoid and clean any draft SMS up.
   1439             deleteDraftSmsMessage(threadId);
   1440         } finally {
   1441             DraftCache.getInstance().setSavingDraft(false);
   1442         }
   1443 
   1444         // Resize all the resizeable attachments (e.g. pictures) to fit
   1445         // in the remaining space in the slideshow.
   1446         int error = 0;
   1447         try {
   1448             slideshow.finalResize(mmsUri);
   1449         } catch (ExceedMessageSizeException e1) {
   1450             error = MESSAGE_SIZE_EXCEEDED;
   1451         } catch (MmsException e1) {
   1452             error = UNKNOWN_ERROR;
   1453         }
   1454         if (error != 0) {
   1455             markMmsMessageWithError(mmsUri);
   1456             mStatusListener.onAttachmentError(error);
   1457             return;
   1458         }
   1459         MessageSender sender = new MmsMessageSender(mActivity, mmsUri,
   1460                 slideshow.getCurrentMessageSize());
   1461         try {
   1462             if (!sender.sendMessage(threadId)) {
   1463                 // The message was sent through SMS protocol, we should
   1464                 // delete the copy which was previously saved in MMS drafts.
   1465                 SqliteWrapper.delete(mActivity, mContentResolver, mmsUri, null, null);
   1466             }
   1467 
   1468             // Make sure this thread isn't over the limits in message count
   1469             Recycler.getMmsRecycler().deleteOldMessagesByThreadId(mActivity, threadId);
   1470         } catch (Exception e) {
   1471             Log.e(TAG, "Failed to send message: " + mmsUri + ", threadId=" + threadId, e);
   1472         }
   1473         MmsWidgetProvider.notifyDatasetChanged(mActivity);
   1474     }
   1475 
   1476     private void markMmsMessageWithError(Uri mmsUri) {
   1477         try {
   1478             PduPersister p = PduPersister.getPduPersister(mActivity);
   1479             // Move the message into MMS Outbox. A trigger will create an entry in
   1480             // the "pending_msgs" table.
   1481             p.move(mmsUri, Mms.Outbox.CONTENT_URI);
   1482 
   1483             // Now update the pending_msgs table with an error for that new item.
   1484             ContentValues values = new ContentValues(1);
   1485             values.put(PendingMessages.ERROR_TYPE, MmsSms.ERR_TYPE_GENERIC_PERMANENT);
   1486             long msgId = ContentUris.parseId(mmsUri);
   1487             SqliteWrapper.update(mActivity, mContentResolver,
   1488                     PendingMessages.CONTENT_URI,
   1489                     values, PendingMessages.MSG_ID + "=" + msgId, null);
   1490         } catch (MmsException e) {
   1491             // Not much we can do here. If the p.move throws an exception, we'll just
   1492             // leave the message in the draft box.
   1493             Log.e(TAG, "Failed to move message to outbox and mark as error: " + mmsUri, e);
   1494         }
   1495     }
   1496 
   1497     // Draft message stuff
   1498 
   1499     private static final String[] MMS_DRAFT_PROJECTION = {
   1500         Mms._ID,                // 0
   1501         Mms.SUBJECT,            // 1
   1502         Mms.SUBJECT_CHARSET     // 2
   1503     };
   1504 
   1505     private static final int MMS_ID_INDEX         = 0;
   1506     private static final int MMS_SUBJECT_INDEX    = 1;
   1507     private static final int MMS_SUBJECT_CS_INDEX = 2;
   1508 
   1509     private static Uri readDraftMmsMessage(Context context, Conversation conv, StringBuilder sb) {
   1510         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1511             LogTag.debug("readDraftMmsMessage conv: " + conv);
   1512         }
   1513         Cursor cursor;
   1514         ContentResolver cr = context.getContentResolver();
   1515 
   1516         final String selection = Mms.THREAD_ID + " = " + conv.getThreadId();
   1517         cursor = SqliteWrapper.query(context, cr,
   1518                 Mms.Draft.CONTENT_URI, MMS_DRAFT_PROJECTION,
   1519                 selection, null, null);
   1520         if (cursor == null) {
   1521             return null;
   1522         }
   1523 
   1524         Uri uri;
   1525         try {
   1526             if (cursor.moveToFirst()) {
   1527                 uri = ContentUris.withAppendedId(Mms.Draft.CONTENT_URI,
   1528                         cursor.getLong(MMS_ID_INDEX));
   1529                 String subject = MessageUtils.extractEncStrFromCursor(cursor, MMS_SUBJECT_INDEX,
   1530                         MMS_SUBJECT_CS_INDEX);
   1531                 if (subject != null) {
   1532                     sb.append(subject);
   1533                 }
   1534                 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1535                     LogTag.debug("readDraftMmsMessage uri: ", uri);
   1536                 }
   1537                 return uri;
   1538             }
   1539         } finally {
   1540             cursor.close();
   1541         }
   1542 
   1543         return null;
   1544     }
   1545 
   1546     /**
   1547      * makeSendReq should always return a non-null SendReq, whether the dest addresses are
   1548      * valid or not.
   1549      */
   1550     private static SendReq makeSendReq(Conversation conv, CharSequence subject) {
   1551         String[] dests = conv.getRecipients().getNumbers(true /* scrub for MMS address */);
   1552 
   1553         SendReq req = new SendReq();
   1554         EncodedStringValue[] encodedNumbers = EncodedStringValue.encodeStrings(dests);
   1555         if (encodedNumbers != null) {
   1556             req.setTo(encodedNumbers);
   1557         }
   1558 
   1559         if (!TextUtils.isEmpty(subject)) {
   1560             req.setSubject(new EncodedStringValue(subject.toString()));
   1561         }
   1562 
   1563         req.setDate(System.currentTimeMillis() / 1000L);
   1564 
   1565         return req;
   1566     }
   1567 
   1568     private static Uri createDraftMmsMessage(PduPersister persister, SendReq sendReq,
   1569             SlideshowModel slideshow, Uri preUri, Context context,
   1570             HashMap<Uri, InputStream> preOpenedFiles) {
   1571         if (slideshow == null) {
   1572             return null;
   1573         }
   1574         try {
   1575             PduBody pb = slideshow.toPduBody();
   1576             sendReq.setBody(pb);
   1577             Uri res = persister.persist(sendReq, preUri == null ? Mms.Draft.CONTENT_URI : preUri,
   1578                     true, MessagingPreferenceActivity.getIsGroupMmsEnabled(context),
   1579                     preOpenedFiles);
   1580             slideshow.sync(pb);
   1581             return res;
   1582         } catch (MmsException e) {
   1583             return null;
   1584         }
   1585     }
   1586 
   1587     private void asyncUpdateDraftMmsMessage(final Conversation conv, final boolean isStopping) {
   1588         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1589             LogTag.debug("asyncUpdateDraftMmsMessage conv=%s mMessageUri=%s", conv, mMessageUri);
   1590         }
   1591         final HashMap<Uri, InputStream> preOpenedFiles =
   1592                 mSlideshow.openPartFiles(mContentResolver);
   1593 
   1594         new Thread(new Runnable() {
   1595             @Override
   1596             public void run() {
   1597                 try {
   1598                     DraftCache.getInstance().setSavingDraft(true);
   1599 
   1600                     final PduPersister persister = PduPersister.getPduPersister(mActivity);
   1601                     final SendReq sendReq = makeSendReq(conv, mSubject);
   1602 
   1603                     if (mMessageUri == null) {
   1604                         mMessageUri = createDraftMmsMessage(persister, sendReq, mSlideshow, null,
   1605                                 mActivity, preOpenedFiles);
   1606                     } else {
   1607                         updateDraftMmsMessage(mMessageUri, persister, mSlideshow, sendReq,
   1608                                 preOpenedFiles);
   1609                     }
   1610                     ensureThreadIdIfNeeded(conv, isStopping);
   1611                     conv.setDraftState(true);
   1612                     if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1613                         LogTag.debug("asyncUpdateDraftMmsMessage conv: " + conv +
   1614                                 " uri: " + mMessageUri);
   1615                     }
   1616 
   1617                     // Be paranoid and delete any SMS drafts that might be lying around. Must do
   1618                     // this after ensureThreadId so conv has the correct thread id.
   1619                     asyncDeleteDraftSmsMessage(conv);
   1620                 } finally {
   1621                     DraftCache.getInstance().setSavingDraft(false);
   1622                     closePreOpenedFiles(preOpenedFiles);
   1623                 }
   1624             }
   1625         }, "WorkingMessage.asyncUpdateDraftMmsMessage").start();
   1626     }
   1627 
   1628     private static void updateDraftMmsMessage(Uri uri, PduPersister persister,
   1629             SlideshowModel slideshow, SendReq sendReq, HashMap<Uri, InputStream> preOpenedFiles) {
   1630         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1631             LogTag.debug("updateDraftMmsMessage uri=%s", uri);
   1632         }
   1633         if (uri == null) {
   1634             Log.e(TAG, "updateDraftMmsMessage null uri");
   1635             return;
   1636         }
   1637         persister.updateHeaders(uri, sendReq);
   1638 
   1639         final PduBody pb = slideshow.toPduBody();
   1640 
   1641         try {
   1642             persister.updateParts(uri, pb, preOpenedFiles);
   1643         } catch (MmsException e) {
   1644             Log.e(TAG, "updateDraftMmsMessage: cannot update message " + uri);
   1645         }
   1646 
   1647         slideshow.sync(pb);
   1648     }
   1649 
   1650     private static void closePreOpenedFiles(HashMap<Uri, InputStream> preOpenedFiles) {
   1651         if (preOpenedFiles == null) {
   1652             return;
   1653         }
   1654         Set<Uri> uris = preOpenedFiles.keySet();
   1655         for (Uri uri : uris) {
   1656             InputStream is = preOpenedFiles.get(uri);
   1657             if (is != null) {
   1658                 try {
   1659                     is.close();
   1660                 } catch (IOException e) {
   1661                 }
   1662             }
   1663         }
   1664     }
   1665 
   1666     private static final String SMS_DRAFT_WHERE = Sms.TYPE + "=" + Sms.MESSAGE_TYPE_DRAFT;
   1667     private static final String[] SMS_BODY_PROJECTION = { Sms.BODY };
   1668     private static final int SMS_BODY_INDEX = 0;
   1669 
   1670     /**
   1671      * Reads a draft message for the given thread ID from the database,
   1672      * if there is one, deletes it from the database, and returns it.
   1673      * @return The draft message or an empty string.
   1674      */
   1675     private String readDraftSmsMessage(Conversation conv) {
   1676         long thread_id = conv.getThreadId();
   1677         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1678             Log.d(TAG, "readDraftSmsMessage conv: " + conv);
   1679         }
   1680         // If it's an invalid thread or we know there's no draft, don't bother.
   1681         if (thread_id <= 0 || !conv.hasDraft()) {
   1682             return "";
   1683         }
   1684 
   1685         Uri thread_uri = ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, thread_id);
   1686         String body = "";
   1687 
   1688         Cursor c = SqliteWrapper.query(mActivity, mContentResolver,
   1689                         thread_uri, SMS_BODY_PROJECTION, SMS_DRAFT_WHERE, null, null);
   1690         boolean haveDraft = false;
   1691         if (c != null) {
   1692             try {
   1693                 if (c.moveToFirst()) {
   1694                     body = c.getString(SMS_BODY_INDEX);
   1695                     haveDraft = true;
   1696                 }
   1697             } finally {
   1698                 c.close();
   1699             }
   1700         }
   1701 
   1702         // We found a draft, and if there are no messages in the conversation,
   1703         // that means we deleted the thread, too. Must reset the thread id
   1704         // so we'll eventually create a new thread.
   1705         if (haveDraft && conv.getMessageCount() == 0) {
   1706             asyncDeleteDraftSmsMessage(conv);
   1707 
   1708             // Clean out drafts for this thread -- if the recipient set changes,
   1709             // we will lose track of the original draft and be unable to delete
   1710             // it later.  The message will be re-saved if necessary upon exit of
   1711             // the activity.
   1712             clearConversation(conv, true);
   1713         }
   1714         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1715             LogTag.debug("readDraftSmsMessage haveDraft: ", !TextUtils.isEmpty(body));
   1716         }
   1717 
   1718         return body;
   1719     }
   1720 
   1721     public void clearConversation(final Conversation conv, boolean resetThreadId) {
   1722         if (resetThreadId && conv.getMessageCount() == 0) {
   1723             if (DEBUG) LogTag.debug("clearConversation calling clearThreadId");
   1724             conv.clearThreadId();
   1725         }
   1726 
   1727         conv.setDraftState(false);
   1728     }
   1729 
   1730     private void asyncUpdateDraftSmsMessage(final Conversation conv, final String contents,
   1731             final boolean isStopping) {
   1732         new Thread(new Runnable() {
   1733             @Override
   1734             public void run() {
   1735                 try {
   1736                     DraftCache.getInstance().setSavingDraft(true);
   1737                     if (conv.getRecipients().isEmpty()) {
   1738                         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1739                             LogTag.debug("asyncUpdateDraftSmsMessage no recipients, not saving");
   1740                         }
   1741                         return;
   1742                     }
   1743                     ensureThreadIdIfNeeded(conv, isStopping);
   1744                     conv.setDraftState(true);
   1745                     updateDraftSmsMessage(conv, contents);
   1746                 } finally {
   1747                     DraftCache.getInstance().setSavingDraft(false);
   1748                 }
   1749             }
   1750         }, "WorkingMessage.asyncUpdateDraftSmsMessage").start();
   1751     }
   1752 
   1753     private void updateDraftSmsMessage(final Conversation conv, String contents) {
   1754         final long threadId = conv.getThreadId();
   1755         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1756             LogTag.debug("updateDraftSmsMessage tid=%d, contents=\"%s\"", threadId, contents);
   1757         }
   1758 
   1759         // If we don't have a valid thread, there's nothing to do.
   1760         if (threadId <= 0) {
   1761             return;
   1762         }
   1763 
   1764         ContentValues values = new ContentValues(3);
   1765         values.put(Sms.THREAD_ID, threadId);
   1766         values.put(Sms.BODY, contents);
   1767         values.put(Sms.TYPE, Sms.MESSAGE_TYPE_DRAFT);
   1768         SqliteWrapper.insert(mActivity, mContentResolver, Sms.CONTENT_URI, values);
   1769         asyncDeleteDraftMmsMessage(conv);
   1770         mMessageUri = null;
   1771     }
   1772 
   1773     private void asyncDelete(final Uri uri, final String selection, final String[] selectionArgs) {
   1774         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1775             LogTag.debug("asyncDelete %s where %s", uri, selection);
   1776         }
   1777         new Thread(new Runnable() {
   1778             @Override
   1779             public void run() {
   1780                 SqliteWrapper.delete(mActivity, mContentResolver, uri, selection, selectionArgs);
   1781             }
   1782         }, "WorkingMessage.asyncDelete").start();
   1783     }
   1784 
   1785     public void asyncDeleteDraftSmsMessage(Conversation conv) {
   1786         mHasSmsDraft = false;
   1787 
   1788         final long threadId = conv.getThreadId();
   1789         if (threadId > 0) {
   1790             asyncDelete(ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, threadId),
   1791                 SMS_DRAFT_WHERE, null);
   1792         }
   1793     }
   1794 
   1795     private void deleteDraftSmsMessage(long threadId) {
   1796         SqliteWrapper.delete(mActivity, mContentResolver,
   1797                 ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, threadId),
   1798                 SMS_DRAFT_WHERE, null);
   1799     }
   1800 
   1801     private void asyncDeleteDraftMmsMessage(Conversation conv) {
   1802         mHasMmsDraft = false;
   1803 
   1804         final long threadId = conv.getThreadId();
   1805         // If the thread id is < 1, then the thread_id in the pdu will be "" or NULL. We have
   1806         // to clear those messages as well as ones with a valid thread id.
   1807         final String where = Mms.THREAD_ID +  (threadId > 0 ? " = " + threadId : " IS NULL");
   1808         asyncDelete(Mms.Draft.CONTENT_URI, where, null);
   1809     }
   1810 
   1811     /**
   1812      * Ensure the thread id in conversation if needed, when we try to save a draft with a orphaned
   1813      * one.
   1814      * @param conv The conversation we are in.
   1815      * @param isStopping Whether we are saving the draft in CMA'a onStop
   1816      */
   1817     private void ensureThreadIdIfNeeded(final Conversation conv, final boolean isStopping) {
   1818         if (isStopping && conv.getMessageCount() == 0) {
   1819             // We need to save the drafts in an unorphaned thread id. When the user goes
   1820             // back to ConversationList while we're saving a draft from CMA's.onStop,
   1821             // ConversationList will delete all threads from the thread table that
   1822             // don't have associated sms or pdu entries. In case our thread got deleted,
   1823             // well call clearThreadId() so ensureThreadId will query the db for the new
   1824             // thread.
   1825             conv.clearThreadId();   // force us to get the updated thread id
   1826         }
   1827         if (!conv.getRecipients().isEmpty()) {
   1828             conv.ensureThreadId();
   1829         }
   1830     }
   1831 }
   1832