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