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() {
    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(), false);
    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();
    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();   // 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         } else {
    498             // Set HAS_ATTACHMENT if we need it.
    499             updateState(HAS_ATTACHMENT, hasAttachment(), true);
    500         }
    501         return result;
    502     }
    503 
    504     /**
    505      * Returns true if this message contains anything worth saving.
    506      */
    507     public boolean isWorthSaving() {
    508         // If it actually contains anything, it's of course not empty.
    509         if (hasText() || hasSubject() || hasAttachment() || hasSlideshow()) {
    510             return true;
    511         }
    512 
    513         // When saveAsMms() has been called, we set FORCE_MMS to represent
    514         // sort of an "invisible attachment" so that the message isn't thrown
    515         // away when we are shipping it off to other activities.
    516         if (isFakeMmsForDraft()) {
    517             return true;
    518         }
    519 
    520         return false;
    521     }
    522 
    523     private void cancelThumbnailLoading() {
    524         int numSlides = mSlideshow != null ? mSlideshow.size() : 0;
    525         if (numSlides > 0) {
    526             ImageModel imgModel = mSlideshow.get(numSlides - 1).getImage();
    527             if (imgModel != null) {
    528                 imgModel.cancelThumbnailLoading();
    529             }
    530         }
    531     }
    532 
    533     /**
    534      * Returns true if FORCE_MMS is set.
    535      * When saveAsMms() has been called, we set FORCE_MMS to represent
    536      * sort of an "invisible attachment" so that the message isn't thrown
    537      * away when we are shipping it off to other activities.
    538      */
    539     public boolean isFakeMmsForDraft() {
    540         return (mMmsState & FORCE_MMS) > 0;
    541     }
    542 
    543     /**
    544      * Makes sure mSlideshow is set up.
    545      */
    546     private void ensureSlideshow() {
    547         if (mSlideshow != null) {
    548             return;
    549         }
    550 
    551         SlideshowModel slideshow = SlideshowModel.createNew(mActivity);
    552         SlideModel slide = new SlideModel(slideshow);
    553         slideshow.add(slide);
    554 
    555         mSlideshow = slideshow;
    556     }
    557 
    558     /**
    559      * Change the message's attachment to the data in the specified Uri.
    560      * Used only for single-slide ("attachment mode") messages. If the attachment fails to
    561      * attach, restore the slide to its original state.
    562      */
    563     private int changeMedia(int type, Uri uri, SlideshowEditor slideShowEditor) {
    564         SlideModel originalSlide = mSlideshow.get(0);
    565         if (originalSlide != null) {
    566             slideShowEditor.removeSlide(0);     // remove the original slide
    567         }
    568         slideShowEditor.addNewSlide(0);
    569         SlideModel slide = mSlideshow.get(0);   // get the new empty slide
    570         int result = OK;
    571 
    572         if (slide == null) {
    573             Log.w(LogTag.TAG, "[WorkingMessage] changeMedia: no slides!");
    574             return result;
    575         }
    576 
    577         // Clear the attachment type since we removed all the attachments. If this isn't cleared
    578         // and the slide.add fails (for instance, a selected video could be too big), we'll be
    579         // left in a state where we think we have an attachment, but it's been removed from the
    580         // slide.
    581         mAttachmentType = TEXT;
    582 
    583         // If we're changing to text, just bail out.
    584         if (type == TEXT) {
    585             return result;
    586         }
    587 
    588         result = internalChangeMedia(type, uri, 0, slideShowEditor);
    589         if (result != OK) {
    590             slideShowEditor.removeSlide(0);             // remove the failed slide
    591             if (originalSlide != null) {
    592                 slideShowEditor.addSlide(0, originalSlide); // restore the original slide.
    593             }
    594         }
    595         return result;
    596     }
    597 
    598     /**
    599      * Add the message's attachment to the data in the specified Uri to a new slide.
    600      */
    601     private int appendMedia(int type, Uri uri, SlideshowEditor slideShowEditor) {
    602         int result = OK;
    603 
    604         // If we're changing to text, just bail out.
    605         if (type == TEXT) {
    606             return result;
    607         }
    608 
    609         // The first time this method is called, mSlideshow.size() is going to be
    610         // one (a newly initialized slideshow has one empty slide). The first time we
    611         // attach the picture/video to that first empty slide. From then on when this
    612         // function is called, we've got to create a new slide and add the picture/video
    613         // to that new slide.
    614         boolean addNewSlide = true;
    615         if (mSlideshow.size() == 1 && !mSlideshow.isSimple()) {
    616             addNewSlide = false;
    617         }
    618         if (addNewSlide) {
    619             if (!slideShowEditor.addNewSlide()) {
    620                 return result;
    621             }
    622         }
    623         int slideNum = mSlideshow.size() - 1;
    624         result = internalChangeMedia(type, uri, slideNum, slideShowEditor);
    625         if (result != OK) {
    626             // We added a new slide and what we attempted to insert on the slide failed.
    627             // Delete that slide, otherwise we could end up with a bunch of blank slides.
    628             // It's ok that we're removing the slide even if we didn't add it (because it was
    629             // the first default slide). If adding the first slide fails, we want to remove it.
    630             slideShowEditor.removeSlide(slideNum);
    631         }
    632         return result;
    633     }
    634 
    635     private int internalChangeMedia(int type, Uri uri, int slideNum,
    636             SlideshowEditor slideShowEditor) {
    637         int result = OK;
    638         try {
    639             if (type == IMAGE) {
    640                 slideShowEditor.changeImage(slideNum, uri);
    641             } else if (type == VIDEO) {
    642                 slideShowEditor.changeVideo(slideNum, uri);
    643             } else if (type == AUDIO) {
    644                 slideShowEditor.changeAudio(slideNum, uri);
    645             } else {
    646                 result = UNSUPPORTED_TYPE;
    647             }
    648         } catch (MmsException e) {
    649             Log.e(TAG, "internalChangeMedia:", e);
    650             result = UNKNOWN_ERROR;
    651         } catch (UnsupportContentTypeException e) {
    652             Log.e(TAG, "internalChangeMedia:", e);
    653             result = UNSUPPORTED_TYPE;
    654         } catch (ExceedMessageSizeException e) {
    655             Log.e(TAG, "internalChangeMedia:", e);
    656             result = MESSAGE_SIZE_EXCEEDED;
    657         } catch (ResolutionException e) {
    658             Log.e(TAG, "internalChangeMedia:", e);
    659             result = IMAGE_TOO_LARGE;
    660         }
    661         return result;
    662     }
    663 
    664     /**
    665      * Returns true if the message has an attachment (including slideshows).
    666      */
    667     public boolean hasAttachment() {
    668         return (mAttachmentType > TEXT);
    669     }
    670 
    671     /**
    672      * Returns the slideshow associated with this message.
    673      */
    674     public SlideshowModel getSlideshow() {
    675         return mSlideshow;
    676     }
    677 
    678     /**
    679      * Returns true if the message has a real slideshow, as opposed to just
    680      * one image attachment, for example.
    681      */
    682     public boolean hasSlideshow() {
    683         return (mAttachmentType == SLIDESHOW);
    684     }
    685 
    686     /**
    687      * Sets the MMS subject of the message.  Passing null indicates that there
    688      * is no subject.  Passing "" will result in an empty subject being added
    689      * to the message, possibly triggering a conversion to MMS.  This extra
    690      * bit of state is needed to support ComposeMessageActivity converting to
    691      * MMS when the user adds a subject.  An empty subject will be removed
    692      * before saving to disk or sending, however.
    693      */
    694     public void setSubject(CharSequence s, boolean notify) {
    695         mSubject = s;
    696         updateState(HAS_SUBJECT, (s != null), notify);
    697     }
    698 
    699     /**
    700      * Returns the MMS subject of the message.
    701      */
    702     public CharSequence getSubject() {
    703         return mSubject;
    704     }
    705 
    706     /**
    707      * Returns true if this message has an MMS subject. A subject has to be more than just
    708      * whitespace.
    709      * @return
    710      */
    711     public boolean hasSubject() {
    712         return mSubject != null && TextUtils.getTrimmedLength(mSubject) > 0;
    713     }
    714 
    715     /**
    716      * Moves the message text into the slideshow.  Should be called any time
    717      * the message is about to be sent or written to disk.
    718      */
    719     private void syncTextToSlideshow() {
    720         if (mSlideshow == null || mSlideshow.size() != 1)
    721             return;
    722 
    723         SlideModel slide = mSlideshow.get(0);
    724         TextModel text;
    725         if (!slide.hasText()) {
    726             // Add a TextModel to slide 0 if one doesn't already exist
    727             text = new TextModel(mActivity, ContentType.TEXT_PLAIN, "text_0.txt",
    728                                            mSlideshow.getLayout().getTextRegion());
    729             slide.add(text);
    730         } else {
    731             // Otherwise just reuse the existing one.
    732             text = slide.getText();
    733         }
    734         text.setText(mText);
    735     }
    736 
    737     /**
    738      * Sets the message text out of the slideshow.  Should be called any time
    739      * a slideshow is loaded from disk.
    740      */
    741     private void syncTextFromSlideshow() {
    742         // Don't sync text for real slideshows.
    743         if (mSlideshow.size() != 1) {
    744             return;
    745         }
    746 
    747         SlideModel slide = mSlideshow.get(0);
    748         if (slide == null || !slide.hasText()) {
    749             return;
    750         }
    751 
    752         mText = slide.getText().getText();
    753     }
    754 
    755     /**
    756      * Removes the subject if it is empty, possibly converting back to SMS.
    757      */
    758     private void removeSubjectIfEmpty(boolean notify) {
    759         if (!hasSubject()) {
    760             setSubject(null, notify);
    761         }
    762     }
    763 
    764     /**
    765      * Gets internal message state ready for storage.  Should be called any
    766      * time the message is about to be sent or written to disk.
    767      */
    768     private void prepareForSave(boolean notify) {
    769         // Make sure our working set of recipients is resolved
    770         // to first-class Contact objects before we save.
    771         syncWorkingRecipients();
    772 
    773         if (hasMmsContentToSave()) {
    774             ensureSlideshow();
    775             syncTextToSlideshow();
    776         }
    777     }
    778 
    779     /**
    780      * Resolve the temporary working set of recipients to a ContactList.
    781      */
    782     public void syncWorkingRecipients() {
    783         if (mWorkingRecipients != null) {
    784             ContactList recipients = ContactList.getByNumbers(mWorkingRecipients, false);
    785             mConversation.setRecipients(recipients);    // resets the threadId to zero
    786             setHasMultipleRecipients(recipients.size() > 1, true);
    787             mWorkingRecipients = null;
    788         }
    789     }
    790 
    791     public String getWorkingRecipients() {
    792         // this function is used for DEBUG only
    793         if (mWorkingRecipients == null) {
    794             return null;
    795         }
    796         ContactList recipients = ContactList.getByNumbers(mWorkingRecipients, false);
    797         return recipients.serialize();
    798     }
    799 
    800     // Call when we've returned from adding an attachment. We're no longer forcing the message
    801     // into a Mms message. At this point we either have the goods to make the message a Mms
    802     // or we don't. No longer fake it.
    803     public void removeFakeMmsForDraft() {
    804         updateState(FORCE_MMS, false, false);
    805     }
    806 
    807     /**
    808      * Force the message to be saved as MMS and return the Uri of the message.
    809      * Typically used when handing a message off to another activity.
    810      */
    811     public Uri saveAsMms(boolean notify) {
    812         if (DEBUG) LogTag.debug("saveAsMms mConversation=%s", mConversation);
    813 
    814         // If we have discarded the message, just bail out.
    815         if (mDiscarded) {
    816             LogTag.warn("saveAsMms mDiscarded: true mConversation: " + mConversation +
    817                     " returning NULL uri and bailing");
    818             return null;
    819         }
    820 
    821         // FORCE_MMS behaves as sort of an "invisible attachment", making
    822         // the message seem non-empty (and thus not discarded).  This bit
    823         // is sticky until the last other MMS bit is removed, at which
    824         // point the message will fall back to SMS.
    825         updateState(FORCE_MMS, true, notify);
    826 
    827         // Collect our state to be written to disk.
    828         prepareForSave(true /* notify */);
    829 
    830         try {
    831             // Make sure we are saving to the correct thread ID.
    832             DraftCache.getInstance().setSavingDraft(true);
    833             if (!mConversation.getRecipients().isEmpty()) {
    834                 mConversation.ensureThreadId();
    835             }
    836             mConversation.setDraftState(true);
    837 
    838             PduPersister persister = PduPersister.getPduPersister(mActivity);
    839             SendReq sendReq = makeSendReq(mConversation, mSubject);
    840 
    841             // If we don't already have a Uri lying around, make a new one.  If we do
    842             // have one already, make sure it is synced to disk.
    843             if (mMessageUri == null) {
    844                 mMessageUri = createDraftMmsMessage(persister, sendReq, mSlideshow, null,
    845                         mActivity, null);
    846             } else {
    847                 updateDraftMmsMessage(mMessageUri, persister, mSlideshow, sendReq, null);
    848             }
    849             mHasMmsDraft = true;
    850         } finally {
    851             DraftCache.getInstance().setSavingDraft(false);
    852         }
    853         return mMessageUri;
    854     }
    855 
    856     /**
    857      * Save this message as a draft in the conversation previously specified
    858      * to {@link setConversation}.
    859      */
    860     public void saveDraft(final boolean isStopping) {
    861         // If we have discarded the message, just bail out.
    862         if (mDiscarded) {
    863             LogTag.warn("saveDraft mDiscarded: true mConversation: " + mConversation +
    864                 " skipping saving draft and bailing");
    865             return;
    866         }
    867 
    868         // Make sure setConversation was called.
    869         if (mConversation == null) {
    870             throw new IllegalStateException("saveDraft() called with no conversation");
    871         }
    872 
    873         if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    874             LogTag.debug("saveDraft for mConversation " + mConversation);
    875         }
    876 
    877         // Get ready to write to disk. But don't notify message status when saving draft
    878         prepareForSave(false /* notify */);
    879 
    880         if (requiresMms()) {
    881             if (hasMmsContentToSave()) {
    882                 asyncUpdateDraftMmsMessage(mConversation, isStopping);
    883                 mHasMmsDraft = true;
    884             }
    885         } else {
    886             String content = mText.toString();
    887 
    888             // bug 2169583: don't bother creating a thread id only to delete the thread
    889             // because the content is empty. When we delete the thread in updateDraftSmsMessage,
    890             // we didn't nullify conv.mThreadId, causing a temperary situation where conv
    891             // is holding onto a thread id that isn't in the database. If a new message arrives
    892             // and takes that thread id (because it's the next thread id to be assigned), the
    893             // new message will be merged with the draft message thread, causing confusion!
    894             if (!TextUtils.isEmpty(content)) {
    895                 asyncUpdateDraftSmsMessage(mConversation, content, isStopping);
    896                 mHasSmsDraft = true;
    897             } else {
    898                 // When there's no associated text message, we have to handle the case where there
    899                 // might have been a previous mms draft for this message. This can happen when a
    900                 // user turns an mms back into a sms, such as creating an mms draft with a picture,
    901                 // then removing the picture.
    902                 asyncDeleteDraftMmsMessage(mConversation);
    903                 mMessageUri = null;
    904             }
    905         }
    906     }
    907 
    908     synchronized public void discard() {
    909         if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    910             LogTag.debug("[WorkingMessage] discard");
    911         }
    912 
    913         if (mDiscarded == true) {
    914             return;
    915         }
    916 
    917         // Mark this message as discarded in order to make saveDraft() no-op.
    918         mDiscarded = true;
    919 
    920         cancelThumbnailLoading();
    921 
    922         // Delete any associated drafts if there are any.
    923         if (mHasMmsDraft) {
    924             asyncDeleteDraftMmsMessage(mConversation);
    925         }
    926         if (mHasSmsDraft) {
    927             asyncDeleteDraftSmsMessage(mConversation);
    928         }
    929         clearConversation(mConversation, true);
    930     }
    931 
    932     public void unDiscard() {
    933         if (DEBUG) LogTag.debug("unDiscard");
    934 
    935         mDiscarded = false;
    936     }
    937 
    938     /**
    939      * Returns true if discard() has been called on this message.
    940      */
    941     public boolean isDiscarded() {
    942         return mDiscarded;
    943     }
    944 
    945     /**
    946      * To be called from our Activity's onSaveInstanceState() to give us a chance
    947      * to stow our state away for later retrieval.
    948      *
    949      * @param bundle The Bundle passed in to onSaveInstanceState
    950      */
    951     public void writeStateToBundle(Bundle bundle) {
    952         if (hasSubject()) {
    953             bundle.putString("subject", mSubject.toString());
    954         }
    955 
    956         if (mMessageUri != null) {
    957             bundle.putParcelable("msg_uri", mMessageUri);
    958         } else if (hasText()) {
    959             bundle.putString("sms_body", mText.toString());
    960         }
    961     }
    962 
    963     /**
    964      * To be called from our Activity's onCreate() if the activity manager
    965      * has given it a Bundle to reinflate
    966      * @param bundle The Bundle passed in to onCreate
    967      */
    968     public void readStateFromBundle(Bundle bundle) {
    969         if (bundle == null) {
    970             return;
    971         }
    972 
    973         String subject = bundle.getString("subject");
    974         setSubject(subject, false);
    975 
    976         Uri uri = (Uri)bundle.getParcelable("msg_uri");
    977         if (uri != null) {
    978             loadFromUri(uri);
    979             return;
    980         } else {
    981             String body = bundle.getString("sms_body");
    982             mText = body;
    983         }
    984     }
    985 
    986     /**
    987      * Update the temporary list of recipients, used when setting up a
    988      * new conversation.  Will be converted to a ContactList on any
    989      * save event (send, save draft, etc.)
    990      */
    991     public void setWorkingRecipients(List<String> numbers) {
    992         mWorkingRecipients = numbers;
    993         String s = null;
    994         if (numbers != null) {
    995             int size = numbers.size();
    996             switch (size) {
    997             case 1:
    998                 s = numbers.get(0);
    999                 break;
   1000             case 0:
   1001                 s = "empty";
   1002                 break;
   1003             default:
   1004                 s = "{...} len=" + size;
   1005             }
   1006         }
   1007     }
   1008 
   1009     private void dumpWorkingRecipients() {
   1010         Log.i(TAG, "-- mWorkingRecipients:");
   1011 
   1012         if (mWorkingRecipients != null) {
   1013             int count = mWorkingRecipients.size();
   1014             for (int i=0; i<count; i++) {
   1015                 Log.i(TAG, "   [" + i + "] " + mWorkingRecipients.get(i));
   1016             }
   1017             Log.i(TAG, "");
   1018         }
   1019     }
   1020 
   1021     public void dump() {
   1022         Log.i(TAG, "WorkingMessage:");
   1023         dumpWorkingRecipients();
   1024         if (mConversation != null) {
   1025             Log.i(TAG, "mConversation: " + mConversation.toString());
   1026         }
   1027     }
   1028 
   1029     /**
   1030      * Set the conversation associated with this message.
   1031      */
   1032     public void setConversation(Conversation conv) {
   1033         if (DEBUG) LogTag.debug("setConversation %s -> %s", mConversation, conv);
   1034 
   1035         mConversation = conv;
   1036 
   1037         // Convert to MMS if there are any email addresses in the recipient list.
   1038         ContactList contactList = conv.getRecipients();
   1039         setHasEmail(contactList.containsEmail(), false);
   1040         setHasMultipleRecipients(contactList.size() > 1, false);
   1041     }
   1042 
   1043     public Conversation getConversation() {
   1044         return mConversation;
   1045     }
   1046 
   1047     /**
   1048      * Hint whether or not this message will be delivered to an
   1049      * an email address.
   1050      */
   1051     public void setHasEmail(boolean hasEmail, boolean notify) {
   1052         if (MmsConfig.getEmailGateway() != null) {
   1053             updateState(RECIPIENTS_REQUIRE_MMS, false, notify);
   1054         } else {
   1055             updateState(RECIPIENTS_REQUIRE_MMS, hasEmail, notify);
   1056         }
   1057     }
   1058     /**
   1059      * Set whether this message will be sent to multiple recipients. This is a hint whether the
   1060      * message needs to be sent as an mms or not. If MmsConfig.getGroupMmsEnabled is false, then
   1061      * the fact that the message is sent to multiple recipients is not a factor in determining
   1062      * whether the message is sent as an mms, but the other factors (such as, "has a picture
   1063      * attachment") still hold true.
   1064      */
   1065     public void setHasMultipleRecipients(boolean hasMultipleRecipients, boolean notify) {
   1066         updateState(MULTIPLE_RECIPIENTS,
   1067                 hasMultipleRecipients &&
   1068                     MessagingPreferenceActivity.getIsGroupMmsEnabled(mActivity),
   1069                 notify);
   1070     }
   1071 
   1072     /**
   1073      * Returns true if this message would require MMS to send.
   1074      */
   1075     public boolean requiresMms() {
   1076         return (mMmsState > 0);
   1077     }
   1078 
   1079     /**
   1080      * Returns true if this message has been turned into an mms because it has a subject or
   1081      * an attachment, but not just because it has multiple recipients.
   1082      */
   1083     private boolean hasMmsContentToSave() {
   1084         if (mMmsState == 0) {
   1085             return false;
   1086         }
   1087         if (mMmsState == MULTIPLE_RECIPIENTS && !hasText()) {
   1088             // If this message is only mms because of multiple recipients and there's no text
   1089             // to save, don't bother saving.
   1090             return false;
   1091         }
   1092         return true;
   1093     }
   1094 
   1095     /**
   1096      * Set whether or not we want to send this message via MMS in order to
   1097      * avoid sending an excessive number of concatenated SMS messages.
   1098      * @param: mmsRequired is the value for the LENGTH_REQUIRES_MMS bit.
   1099      * @param: notify Whether or not to notify the user.
   1100     */
   1101     public void setLengthRequiresMms(boolean mmsRequired, boolean notify) {
   1102         updateState(LENGTH_REQUIRES_MMS, mmsRequired, notify);
   1103     }
   1104 
   1105     private static String stateString(int state) {
   1106         if (state == 0)
   1107             return "<none>";
   1108 
   1109         StringBuilder sb = new StringBuilder();
   1110         if ((state & RECIPIENTS_REQUIRE_MMS) > 0)
   1111             sb.append("RECIPIENTS_REQUIRE_MMS | ");
   1112         if ((state & HAS_SUBJECT) > 0)
   1113             sb.append("HAS_SUBJECT | ");
   1114         if ((state & HAS_ATTACHMENT) > 0)
   1115             sb.append("HAS_ATTACHMENT | ");
   1116         if ((state & LENGTH_REQUIRES_MMS) > 0)
   1117             sb.append("LENGTH_REQUIRES_MMS | ");
   1118         if ((state & FORCE_MMS) > 0)
   1119             sb.append("FORCE_MMS | ");
   1120         if ((state & MULTIPLE_RECIPIENTS) > 0)
   1121             sb.append("MULTIPLE_RECIPIENTS | ");
   1122 
   1123         sb.delete(sb.length() - 3, sb.length());
   1124         return sb.toString();
   1125     }
   1126 
   1127     /**
   1128      * Sets the current state of our various "MMS required" bits.
   1129      *
   1130      * @param state The bit to change, such as {@link HAS_ATTACHMENT}
   1131      * @param on If true, set it; if false, clear it
   1132      * @param notify Whether or not to notify the user
   1133      */
   1134     private void updateState(int state, boolean on, boolean notify) {
   1135         if (!sMmsEnabled) {
   1136             // If Mms isn't enabled, the rest of the Messaging UI should not be using any
   1137             // feature that would cause us to to turn on any Mms flag and show the
   1138             // "Converting to multimedia..." message.
   1139             return;
   1140         }
   1141         int oldState = mMmsState;
   1142         if (on) {
   1143             mMmsState |= state;
   1144         } else {
   1145             mMmsState &= ~state;
   1146         }
   1147 
   1148         // If we are clearing the last bit that is not FORCE_MMS,
   1149         // expire the FORCE_MMS bit.
   1150         if (mMmsState == FORCE_MMS && ((oldState & ~FORCE_MMS) > 0)) {
   1151             mMmsState = 0;
   1152         }
   1153 
   1154         // Notify the listener if we are moving from SMS to MMS
   1155         // or vice versa.
   1156         if (notify) {
   1157             if (oldState == 0 && mMmsState != 0) {
   1158                 mStatusListener.onProtocolChanged(true);
   1159             } else if (oldState != 0 && mMmsState == 0) {
   1160                 mStatusListener.onProtocolChanged(false);
   1161             }
   1162         }
   1163 
   1164         if (oldState != mMmsState) {
   1165             if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) LogTag.debug("updateState: %s%s = %s",
   1166                     on ? "+" : "-",
   1167                     stateString(state), stateString(mMmsState));
   1168         }
   1169     }
   1170 
   1171     /**
   1172      * Send this message over the network.  Will call back with onMessageSent() once
   1173      * it has been dispatched to the telephony stack.  This WorkingMessage object is
   1174      * no longer useful after this method has been called.
   1175      *
   1176      * @throws ContentRestrictionException if sending an MMS and uaProfUrl is not defined
   1177      * in mms_config.xml.
   1178      */
   1179     public void send(final String recipientsInUI) {
   1180         long origThreadId = mConversation.getThreadId();
   1181 
   1182         if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
   1183             LogTag.debug("send origThreadId: " + origThreadId);
   1184         }
   1185 
   1186         removeSubjectIfEmpty(true /* notify */);
   1187 
   1188         // Get ready to write to disk.
   1189         prepareForSave(true /* notify */);
   1190 
   1191         // We need the recipient list for both SMS and MMS.
   1192         final Conversation conv = mConversation;
   1193         String msgTxt = mText.toString();
   1194 
   1195         if (requiresMms() || addressContainsEmailToMms(conv, msgTxt)) {
   1196             // uaProfUrl setting in mms_config.xml must be present to send an MMS.
   1197             // However, SMS service will still work in the absence of a uaProfUrl address.
   1198             if (MmsConfig.getUaProfUrl() == null) {
   1199                 String err = "WorkingMessage.send MMS sending failure. mms_config.xml is " +
   1200                         "missing uaProfUrl setting.  uaProfUrl is required for MMS service, " +
   1201                         "but can be absent for SMS.";
   1202                 RuntimeException ex = new NullPointerException(err);
   1203                 Log.e(TAG, err, ex);
   1204                 // now, let's just crash.
   1205                 throw ex;
   1206             }
   1207 
   1208             // Make local copies of the bits we need for sending a message,
   1209             // because we will be doing it off of the main thread, which will
   1210             // immediately continue on to resetting some of this state.
   1211             final Uri mmsUri = mMessageUri;
   1212             final PduPersister persister = PduPersister.getPduPersister(mActivity);
   1213 
   1214             final SlideshowModel slideshow = mSlideshow;
   1215             final CharSequence subject = mSubject;
   1216             final boolean textOnly = mAttachmentType == TEXT;
   1217 
   1218             if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
   1219                 LogTag.debug("Send mmsUri: " + mmsUri);
   1220             }
   1221 
   1222             // Do the dirty work of sending the message off of the main UI thread.
   1223             new Thread(new Runnable() {
   1224                 @Override
   1225                 public void run() {
   1226                     final SendReq sendReq = makeSendReq(conv, subject);
   1227 
   1228                     // Make sure the text in slide 0 is no longer holding onto a reference to
   1229                     // the text in the message text box.
   1230                     slideshow.prepareForSend();
   1231                     sendMmsWorker(conv, mmsUri, persister, slideshow, sendReq, textOnly);
   1232 
   1233                     updateSendStats(conv);
   1234                 }
   1235             }, "WorkingMessage.send MMS").start();
   1236         } else {
   1237             // Same rules apply as above.
   1238             final String msgText = mText.toString();
   1239             new Thread(new Runnable() {
   1240                 @Override
   1241                 public void run() {
   1242                     preSendSmsWorker(conv, msgText, recipientsInUI);
   1243 
   1244                     updateSendStats(conv);
   1245                 }
   1246             }, "WorkingMessage.send SMS").start();
   1247         }
   1248 
   1249         // update the Recipient cache with the new to address, if it's different
   1250         RecipientIdCache.updateNumbers(conv.getThreadId(), conv.getRecipients());
   1251 
   1252         // Mark the message as discarded because it is "off the market" after being sent.
   1253         mDiscarded = true;
   1254     }
   1255 
   1256     // Be sure to only call this on a background thread.
   1257     private void updateSendStats(final Conversation conv) {
   1258         String[] dests = conv.getRecipients().getNumbers();
   1259         final ArrayList<String> phoneNumbers = new ArrayList<String>(Arrays.asList(dests));
   1260 
   1261         DataUsageStatUpdater updater = new DataUsageStatUpdater(mActivity);
   1262         updater.updateWithPhoneNumber(phoneNumbers);
   1263     }
   1264 
   1265     private boolean addressContainsEmailToMms(Conversation conv, String text) {
   1266         if (MmsConfig.getEmailGateway() != null) {
   1267             String[] dests = conv.getRecipients().getNumbers();
   1268             int length = dests.length;
   1269             for (int i = 0; i < length; i++) {
   1270                 if (Mms.isEmailAddress(dests[i]) || MessageUtils.isAlias(dests[i])) {
   1271                     String mtext = dests[i] + " " + text;
   1272                     int[] params = SmsMessage.calculateLength(mtext, false);
   1273                     if (params[0] > 1) {
   1274                         updateState(RECIPIENTS_REQUIRE_MMS, true, true);
   1275                         ensureSlideshow();
   1276                         syncTextToSlideshow();
   1277                         return true;
   1278                     }
   1279                 }
   1280             }
   1281         }
   1282         return false;
   1283     }
   1284 
   1285     // Message sending stuff
   1286 
   1287     private void preSendSmsWorker(Conversation conv, String msgText, String recipientsInUI) {
   1288         // If user tries to send the message, it's a signal the inputted text is what they wanted.
   1289         UserHappinessSignals.userAcceptedImeText(mActivity);
   1290 
   1291         mStatusListener.onPreMessageSent();
   1292 
   1293         long origThreadId = conv.getThreadId();
   1294 
   1295         // Make sure we are still using the correct thread ID for our recipient set.
   1296         long threadId = conv.ensureThreadId();
   1297 
   1298         String semiSepRecipients = conv.getRecipients().serialize();
   1299 
   1300         // recipientsInUI can be empty when the user types in a number and hits send
   1301         if (LogTag.SEVERE_WARNING && ((origThreadId != 0 && origThreadId != threadId) ||
   1302                (!semiSepRecipients.equals(recipientsInUI) && !TextUtils.isEmpty(recipientsInUI)))) {
   1303             String msg = origThreadId != 0 && origThreadId != threadId ?
   1304                     "WorkingMessage.preSendSmsWorker threadId changed or " +
   1305                     "recipients changed. origThreadId: " +
   1306                     origThreadId + " new threadId: " + threadId +
   1307                     " also mConversation.getThreadId(): " +
   1308                     mConversation.getThreadId()
   1309                 :
   1310                     "Recipients in window: \"" +
   1311                     recipientsInUI + "\" differ from recipients from conv: \"" +
   1312                     semiSepRecipients + "\"";
   1313 
   1314             LogTag.warnPossibleRecipientMismatch(msg, mActivity);
   1315         }
   1316 
   1317         // just do a regular send. We're already on a non-ui thread so no need to fire
   1318         // off another thread to do this work.
   1319         sendSmsWorker(msgText, semiSepRecipients, threadId);
   1320 
   1321         // Be paranoid and clean any draft SMS up.
   1322         deleteDraftSmsMessage(threadId);
   1323     }
   1324 
   1325     private void sendSmsWorker(String msgText, String semiSepRecipients, long threadId) {
   1326         String[] dests = TextUtils.split(semiSepRecipients, ";");
   1327         if (LogTag.VERBOSE || Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
   1328             Log.d(LogTag.TRANSACTION, "sendSmsWorker sending message: recipients=" +
   1329                     semiSepRecipients + ", threadId=" + threadId);
   1330         }
   1331         MessageSender sender = new SmsMessageSender(mActivity, dests, msgText, threadId);
   1332         try {
   1333             sender.sendMessage(threadId);
   1334 
   1335             // Make sure this thread isn't over the limits in message count
   1336             Recycler.getSmsRecycler().deleteOldMessagesByThreadId(mActivity, threadId);
   1337         } catch (Exception e) {
   1338             Log.e(TAG, "Failed to send SMS message, threadId=" + threadId, e);
   1339         }
   1340 
   1341         mStatusListener.onMessageSent();
   1342         MmsWidgetProvider.notifyDatasetChanged(mActivity);
   1343     }
   1344 
   1345     private void sendMmsWorker(Conversation conv, Uri mmsUri, PduPersister persister,
   1346             SlideshowModel slideshow, SendReq sendReq, boolean textOnly) {
   1347         long threadId = 0;
   1348         Cursor cursor = null;
   1349         boolean newMessage = false;
   1350         try {
   1351             // Put a placeholder message in the database first
   1352             DraftCache.getInstance().setSavingDraft(true);
   1353             mStatusListener.onPreMessageSent();
   1354 
   1355             // Make sure we are still using the correct thread ID for our
   1356             // recipient set.
   1357             threadId = conv.ensureThreadId();
   1358 
   1359             if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1360                 LogTag.debug("sendMmsWorker: update draft MMS message " + mmsUri +
   1361                         " threadId: " + threadId);
   1362             }
   1363 
   1364             // One last check to verify the address of the recipient.
   1365             String[] dests = conv.getRecipients().getNumbers(true /* scrub for MMS address */);
   1366             if (dests.length == 1) {
   1367                 // verify the single address matches what's in the database. If we get a different
   1368                 // address back, jam the new value back into the SendReq.
   1369                 String newAddress =
   1370                     Conversation.verifySingleRecipient(mActivity, conv.getThreadId(), dests[0]);
   1371 
   1372                 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1373                     LogTag.debug("sendMmsWorker: newAddress " + newAddress +
   1374                             " dests[0]: " + dests[0]);
   1375                 }
   1376 
   1377                 if (!newAddress.equals(dests[0])) {
   1378                     dests[0] = newAddress;
   1379                     EncodedStringValue[] encodedNumbers = EncodedStringValue.encodeStrings(dests);
   1380                     if (encodedNumbers != null) {
   1381                         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1382                             LogTag.debug("sendMmsWorker: REPLACING number!!!");
   1383                         }
   1384                         sendReq.setTo(encodedNumbers);
   1385                     }
   1386                 }
   1387             }
   1388             newMessage = mmsUri == null;
   1389             if (newMessage) {
   1390                 // Write something in the database so the new message will appear as sending
   1391                 ContentValues values = new ContentValues();
   1392                 values.put(Mms.MESSAGE_BOX, Mms.MESSAGE_BOX_OUTBOX);
   1393                 values.put(Mms.THREAD_ID, threadId);
   1394                 values.put(Mms.MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_SEND_REQ);
   1395                 if (textOnly) {
   1396                     values.put(Mms.TEXT_ONLY, 1);
   1397                 }
   1398                 mmsUri = SqliteWrapper.insert(mActivity, mContentResolver, Mms.Outbox.CONTENT_URI,
   1399                         values);
   1400             }
   1401             mStatusListener.onMessageSent();
   1402 
   1403             // If user tries to send the message, it's a signal the inputted text is
   1404             // what they wanted.
   1405             UserHappinessSignals.userAcceptedImeText(mActivity);
   1406 
   1407             // First make sure we don't have too many outstanding unsent message.
   1408             cursor = SqliteWrapper.query(mActivity, mContentResolver,
   1409                     Mms.Outbox.CONTENT_URI, MMS_OUTBOX_PROJECTION, null, null, null);
   1410             if (cursor != null) {
   1411                 long maxMessageSize = MmsConfig.getMaxSizeScaleForPendingMmsAllowed() *
   1412                 MmsConfig.getMaxMessageSize();
   1413                 long totalPendingSize = 0;
   1414                 while (cursor.moveToNext()) {
   1415                     totalPendingSize += cursor.getLong(MMS_MESSAGE_SIZE_INDEX);
   1416                 }
   1417                 if (totalPendingSize >= maxMessageSize) {
   1418                     unDiscard();    // it wasn't successfully sent. Allow it to be saved as a draft.
   1419                     mStatusListener.onMaxPendingMessagesReached();
   1420                     markMmsMessageWithError(mmsUri);
   1421                     return;
   1422                 }
   1423             }
   1424         } finally {
   1425             if (cursor != null) {
   1426                 cursor.close();
   1427             }
   1428         }
   1429 
   1430         try {
   1431             if (newMessage) {
   1432                 // Create a new MMS message if one hasn't been made yet.
   1433                 mmsUri = createDraftMmsMessage(persister, sendReq, slideshow, mmsUri,
   1434                         mActivity, null);
   1435             } else {
   1436                 // Otherwise, sync the MMS message in progress to disk.
   1437                 updateDraftMmsMessage(mmsUri, persister, slideshow, sendReq, null);
   1438             }
   1439 
   1440             // Be paranoid and clean any draft SMS up.
   1441             deleteDraftSmsMessage(threadId);
   1442         } finally {
   1443             DraftCache.getInstance().setSavingDraft(false);
   1444         }
   1445 
   1446         // Resize all the resizeable attachments (e.g. pictures) to fit
   1447         // in the remaining space in the slideshow.
   1448         int error = 0;
   1449         try {
   1450             slideshow.finalResize(mmsUri);
   1451         } catch (ExceedMessageSizeException e1) {
   1452             error = MESSAGE_SIZE_EXCEEDED;
   1453         } catch (MmsException e1) {
   1454             error = UNKNOWN_ERROR;
   1455         }
   1456         if (error != 0) {
   1457             markMmsMessageWithError(mmsUri);
   1458             mStatusListener.onAttachmentError(error);
   1459             return;
   1460         }
   1461         MessageSender sender = new MmsMessageSender(mActivity, mmsUri,
   1462                 slideshow.getCurrentMessageSize());
   1463         try {
   1464             if (!sender.sendMessage(threadId)) {
   1465                 // The message was sent through SMS protocol, we should
   1466                 // delete the copy which was previously saved in MMS drafts.
   1467                 SqliteWrapper.delete(mActivity, mContentResolver, mmsUri, null, null);
   1468             }
   1469 
   1470             // Make sure this thread isn't over the limits in message count
   1471             Recycler.getMmsRecycler().deleteOldMessagesByThreadId(mActivity, threadId);
   1472         } catch (Exception e) {
   1473             Log.e(TAG, "Failed to send message: " + mmsUri + ", threadId=" + threadId, e);
   1474         }
   1475         MmsWidgetProvider.notifyDatasetChanged(mActivity);
   1476     }
   1477 
   1478     private void markMmsMessageWithError(Uri mmsUri) {
   1479         try {
   1480             PduPersister p = PduPersister.getPduPersister(mActivity);
   1481             // Move the message into MMS Outbox. A trigger will create an entry in
   1482             // the "pending_msgs" table.
   1483             p.move(mmsUri, Mms.Outbox.CONTENT_URI);
   1484 
   1485             // Now update the pending_msgs table with an error for that new item.
   1486             ContentValues values = new ContentValues(1);
   1487             values.put(PendingMessages.ERROR_TYPE, MmsSms.ERR_TYPE_GENERIC_PERMANENT);
   1488             long msgId = ContentUris.parseId(mmsUri);
   1489             SqliteWrapper.update(mActivity, mContentResolver,
   1490                     PendingMessages.CONTENT_URI,
   1491                     values, PendingMessages.MSG_ID + "=" + msgId, null);
   1492         } catch (MmsException e) {
   1493             // Not much we can do here. If the p.move throws an exception, we'll just
   1494             // leave the message in the draft box.
   1495             Log.e(TAG, "Failed to move message to outbox and mark as error: " + mmsUri, e);
   1496         }
   1497     }
   1498 
   1499     // Draft message stuff
   1500 
   1501     private static final String[] MMS_DRAFT_PROJECTION = {
   1502         Mms._ID,                // 0
   1503         Mms.SUBJECT,            // 1
   1504         Mms.SUBJECT_CHARSET     // 2
   1505     };
   1506 
   1507     private static final int MMS_ID_INDEX         = 0;
   1508     private static final int MMS_SUBJECT_INDEX    = 1;
   1509     private static final int MMS_SUBJECT_CS_INDEX = 2;
   1510 
   1511     private static Uri readDraftMmsMessage(Context context, Conversation conv, StringBuilder sb) {
   1512         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1513             LogTag.debug("readDraftMmsMessage conv: " + conv);
   1514         }
   1515         Cursor cursor;
   1516         ContentResolver cr = context.getContentResolver();
   1517 
   1518         final String selection = Mms.THREAD_ID + " = " + conv.getThreadId();
   1519         cursor = SqliteWrapper.query(context, cr,
   1520                 Mms.Draft.CONTENT_URI, MMS_DRAFT_PROJECTION,
   1521                 selection, null, null);
   1522 
   1523         Uri uri;
   1524         try {
   1525             if (cursor.moveToFirst()) {
   1526                 uri = ContentUris.withAppendedId(Mms.Draft.CONTENT_URI,
   1527                         cursor.getLong(MMS_ID_INDEX));
   1528                 String subject = MessageUtils.extractEncStrFromCursor(cursor, MMS_SUBJECT_INDEX,
   1529                         MMS_SUBJECT_CS_INDEX);
   1530                 if (subject != null) {
   1531                     sb.append(subject);
   1532                 }
   1533                 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1534                     LogTag.debug("readDraftMmsMessage uri: ", uri);
   1535                 }
   1536                 return uri;
   1537             }
   1538         } finally {
   1539             cursor.close();
   1540         }
   1541 
   1542         return null;
   1543     }
   1544 
   1545     /**
   1546      * makeSendReq should always return a non-null SendReq, whether the dest addresses are
   1547      * valid or not.
   1548      */
   1549     private static SendReq makeSendReq(Conversation conv, CharSequence subject) {
   1550         String[] dests = conv.getRecipients().getNumbers(true /* scrub for MMS address */);
   1551 
   1552         SendReq req = new SendReq();
   1553         EncodedStringValue[] encodedNumbers = EncodedStringValue.encodeStrings(dests);
   1554         if (encodedNumbers != null) {
   1555             req.setTo(encodedNumbers);
   1556         }
   1557 
   1558         if (!TextUtils.isEmpty(subject)) {
   1559             req.setSubject(new EncodedStringValue(subject.toString()));
   1560         }
   1561 
   1562         req.setDate(System.currentTimeMillis() / 1000L);
   1563 
   1564         return req;
   1565     }
   1566 
   1567     private static Uri createDraftMmsMessage(PduPersister persister, SendReq sendReq,
   1568             SlideshowModel slideshow, Uri preUri, Context context,
   1569             HashMap<Uri, InputStream> preOpenedFiles) {
   1570         if (slideshow == null) {
   1571             return null;
   1572         }
   1573         try {
   1574             PduBody pb = slideshow.toPduBody();
   1575             sendReq.setBody(pb);
   1576             Uri res = persister.persist(sendReq, preUri == null ? Mms.Draft.CONTENT_URI : preUri,
   1577                     true, MessagingPreferenceActivity.getIsGroupMmsEnabled(context),
   1578                     preOpenedFiles);
   1579             slideshow.sync(pb);
   1580             return res;
   1581         } catch (MmsException e) {
   1582             return null;
   1583         }
   1584     }
   1585 
   1586     private void asyncUpdateDraftMmsMessage(final Conversation conv, final boolean isStopping) {
   1587         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1588             LogTag.debug("asyncUpdateDraftMmsMessage conv=%s mMessageUri=%s", conv, mMessageUri);
   1589         }
   1590         final HashMap<Uri, InputStream> preOpenedFiles =
   1591                 mSlideshow.openPartFiles(mContentResolver);
   1592 
   1593         new Thread(new Runnable() {
   1594             @Override
   1595             public void run() {
   1596                 try {
   1597                     DraftCache.getInstance().setSavingDraft(true);
   1598 
   1599                     final PduPersister persister = PduPersister.getPduPersister(mActivity);
   1600                     final SendReq sendReq = makeSendReq(conv, mSubject);
   1601 
   1602                     if (mMessageUri == null) {
   1603                         mMessageUri = createDraftMmsMessage(persister, sendReq, mSlideshow, null,
   1604                                 mActivity, preOpenedFiles);
   1605                     } else {
   1606                         updateDraftMmsMessage(mMessageUri, persister, mSlideshow, sendReq,
   1607                                 preOpenedFiles);
   1608                     }
   1609                     ensureThreadIdIfNeeded(conv, isStopping);
   1610                     conv.setDraftState(true);
   1611                     if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1612                         LogTag.debug("asyncUpdateDraftMmsMessage conv: " + conv +
   1613                                 " uri: " + mMessageUri);
   1614                     }
   1615 
   1616                     // Be paranoid and delete any SMS drafts that might be lying around. Must do
   1617                     // this after ensureThreadId so conv has the correct thread id.
   1618                     asyncDeleteDraftSmsMessage(conv);
   1619                 } finally {
   1620                     DraftCache.getInstance().setSavingDraft(false);
   1621                     closePreOpenedFiles(preOpenedFiles);
   1622                 }
   1623             }
   1624         }, "WorkingMessage.asyncUpdateDraftMmsMessage").start();
   1625     }
   1626 
   1627     private static void updateDraftMmsMessage(Uri uri, PduPersister persister,
   1628             SlideshowModel slideshow, SendReq sendReq, HashMap<Uri, InputStream> preOpenedFiles) {
   1629         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1630             LogTag.debug("updateDraftMmsMessage uri=%s", uri);
   1631         }
   1632         if (uri == null) {
   1633             Log.e(TAG, "updateDraftMmsMessage null uri");
   1634             return;
   1635         }
   1636         persister.updateHeaders(uri, sendReq);
   1637 
   1638         final PduBody pb = slideshow.toPduBody();
   1639 
   1640         try {
   1641             persister.updateParts(uri, pb, preOpenedFiles);
   1642         } catch (MmsException e) {
   1643             Log.e(TAG, "updateDraftMmsMessage: cannot update message " + uri);
   1644         }
   1645 
   1646         slideshow.sync(pb);
   1647     }
   1648 
   1649     private static void closePreOpenedFiles(HashMap<Uri, InputStream> preOpenedFiles) {
   1650         if (preOpenedFiles == null) {
   1651             return;
   1652         }
   1653         Set<Uri> uris = preOpenedFiles.keySet();
   1654         for (Uri uri : uris) {
   1655             InputStream is = preOpenedFiles.get(uri);
   1656             if (is != null) {
   1657                 try {
   1658                     is.close();
   1659                 } catch (IOException e) {
   1660                 }
   1661             }
   1662         }
   1663     }
   1664 
   1665     private static final String SMS_DRAFT_WHERE = Sms.TYPE + "=" + Sms.MESSAGE_TYPE_DRAFT;
   1666     private static final String[] SMS_BODY_PROJECTION = { Sms.BODY };
   1667     private static final int SMS_BODY_INDEX = 0;
   1668 
   1669     /**
   1670      * Reads a draft message for the given thread ID from the database,
   1671      * if there is one, deletes it from the database, and returns it.
   1672      * @return The draft message or an empty string.
   1673      */
   1674     private String readDraftSmsMessage(Conversation conv) {
   1675         long thread_id = conv.getThreadId();
   1676         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1677             Log.d(TAG, "readDraftSmsMessage conv: " + conv);
   1678         }
   1679         // If it's an invalid thread or we know there's no draft, don't bother.
   1680         if (thread_id <= 0 || !conv.hasDraft()) {
   1681             return "";
   1682         }
   1683 
   1684         Uri thread_uri = ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, thread_id);
   1685         String body = "";
   1686 
   1687         Cursor c = SqliteWrapper.query(mActivity, mContentResolver,
   1688                         thread_uri, SMS_BODY_PROJECTION, SMS_DRAFT_WHERE, null, null);
   1689         boolean haveDraft = false;
   1690         if (c != null) {
   1691             try {
   1692                 if (c.moveToFirst()) {
   1693                     body = c.getString(SMS_BODY_INDEX);
   1694                     haveDraft = true;
   1695                 }
   1696             } finally {
   1697                 c.close();
   1698             }
   1699         }
   1700 
   1701         // We found a draft, and if there are no messages in the conversation,
   1702         // that means we deleted the thread, too. Must reset the thread id
   1703         // so we'll eventually create a new thread.
   1704         if (haveDraft && conv.getMessageCount() == 0) {
   1705             asyncDeleteDraftSmsMessage(conv);
   1706 
   1707             // Clean out drafts for this thread -- if the recipient set changes,
   1708             // we will lose track of the original draft and be unable to delete
   1709             // it later.  The message will be re-saved if necessary upon exit of
   1710             // the activity.
   1711             clearConversation(conv, true);
   1712         }
   1713         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1714             LogTag.debug("readDraftSmsMessage haveDraft: ", !TextUtils.isEmpty(body));
   1715         }
   1716 
   1717         return body;
   1718     }
   1719 
   1720     public void clearConversation(final Conversation conv, boolean resetThreadId) {
   1721         if (resetThreadId && conv.getMessageCount() == 0) {
   1722             if (DEBUG) LogTag.debug("clearConversation calling clearThreadId");
   1723             conv.clearThreadId();
   1724         }
   1725 
   1726         conv.setDraftState(false);
   1727     }
   1728 
   1729     private void asyncUpdateDraftSmsMessage(final Conversation conv, final String contents,
   1730             final boolean isStopping) {
   1731         new Thread(new Runnable() {
   1732             @Override
   1733             public void run() {
   1734                 try {
   1735                     DraftCache.getInstance().setSavingDraft(true);
   1736                     if (conv.getRecipients().isEmpty()) {
   1737                         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1738                             LogTag.debug("asyncUpdateDraftSmsMessage no recipients, not saving");
   1739                         }
   1740                         return;
   1741                     }
   1742                     ensureThreadIdIfNeeded(conv, isStopping);
   1743                     conv.setDraftState(true);
   1744                     updateDraftSmsMessage(conv, contents);
   1745                 } finally {
   1746                     DraftCache.getInstance().setSavingDraft(false);
   1747                 }
   1748             }
   1749         }, "WorkingMessage.asyncUpdateDraftSmsMessage").start();
   1750     }
   1751 
   1752     private void updateDraftSmsMessage(final Conversation conv, String contents) {
   1753         final long threadId = conv.getThreadId();
   1754         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1755             LogTag.debug("updateDraftSmsMessage tid=%d, contents=\"%s\"", threadId, contents);
   1756         }
   1757 
   1758         // If we don't have a valid thread, there's nothing to do.
   1759         if (threadId <= 0) {
   1760             return;
   1761         }
   1762 
   1763         ContentValues values = new ContentValues(3);
   1764         values.put(Sms.THREAD_ID, threadId);
   1765         values.put(Sms.BODY, contents);
   1766         values.put(Sms.TYPE, Sms.MESSAGE_TYPE_DRAFT);
   1767         SqliteWrapper.insert(mActivity, mContentResolver, Sms.CONTENT_URI, values);
   1768         asyncDeleteDraftMmsMessage(conv);
   1769         mMessageUri = null;
   1770     }
   1771 
   1772     private void asyncDelete(final Uri uri, final String selection, final String[] selectionArgs) {
   1773         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1774             LogTag.debug("asyncDelete %s where %s", uri, selection);
   1775         }
   1776         new Thread(new Runnable() {
   1777             @Override
   1778             public void run() {
   1779                 SqliteWrapper.delete(mActivity, mContentResolver, uri, selection, selectionArgs);
   1780             }
   1781         }, "WorkingMessage.asyncDelete").start();
   1782     }
   1783 
   1784     public void asyncDeleteDraftSmsMessage(Conversation conv) {
   1785         mHasSmsDraft = false;
   1786 
   1787         final long threadId = conv.getThreadId();
   1788         if (threadId > 0) {
   1789             asyncDelete(ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, threadId),
   1790                 SMS_DRAFT_WHERE, null);
   1791         }
   1792     }
   1793 
   1794     private void deleteDraftSmsMessage(long threadId) {
   1795         SqliteWrapper.delete(mActivity, mContentResolver,
   1796                 ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, threadId),
   1797                 SMS_DRAFT_WHERE, null);
   1798     }
   1799 
   1800     private void asyncDeleteDraftMmsMessage(Conversation conv) {
   1801         mHasMmsDraft = false;
   1802 
   1803         final long threadId = conv.getThreadId();
   1804         // If the thread id is < 1, then the thread_id in the pdu will be "" or NULL. We have
   1805         // to clear those messages as well as ones with a valid thread id.
   1806         final String where = Mms.THREAD_ID +  (threadId > 0 ? " = " + threadId : " IS NULL");
   1807         asyncDelete(Mms.Draft.CONTENT_URI, where, null);
   1808     }
   1809 
   1810     /**
   1811      * Ensure the thread id in conversation if needed, when we try to save a draft with a orphaned
   1812      * one.
   1813      * @param conv The conversation we are in.
   1814      * @param isStopping Whether we are saving the draft in CMA'a onStop
   1815      */
   1816     private void ensureThreadIdIfNeeded(final Conversation conv, final boolean isStopping) {
   1817         if (isStopping && conv.getMessageCount() == 0) {
   1818             // We need to save the drafts in an unorphaned thread id. When the user goes
   1819             // back to ConversationList while we're saving a draft from CMA's.onStop,
   1820             // ConversationList will delete all threads from the thread table that
   1821             // don't have associated sms or pdu entries. In case our thread got deleted,
   1822             // well call clearThreadId() so ensureThreadId will query the db for the new
   1823             // thread.
   1824             conv.clearThreadId();   // force us to get the updated thread id
   1825         }
   1826         if (!conv.getRecipients().isEmpty()) {
   1827             conv.ensureThreadId();
   1828         }
   1829     }
   1830 }
   1831