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             }
    831         }
    832 
    833         // Update state of the draft cache.
    834         mConversation.setDraftState(true);
    835     }
    836 
    837     synchronized public void discard() {
    838         if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    839             LogTag.debug("[WorkingMessage] discard");
    840         }
    841 
    842         if (mDiscarded == true) {
    843             return;
    844         }
    845 
    846         // Mark this message as discarded in order to make saveDraft() no-op.
    847         mDiscarded = true;
    848 
    849         // Delete any associated drafts if there are any.
    850         if (mHasMmsDraft) {
    851             asyncDeleteDraftMmsMessage(mConversation);
    852         }
    853         if (mHasSmsDraft) {
    854             asyncDeleteDraftSmsMessage(mConversation);
    855         }
    856         clearConversation(mConversation, true);
    857     }
    858 
    859     public void unDiscard() {
    860         if (DEBUG) LogTag.debug("unDiscard");
    861 
    862         mDiscarded = false;
    863     }
    864 
    865     /**
    866      * Returns true if discard() has been called on this message.
    867      */
    868     public boolean isDiscarded() {
    869         return mDiscarded;
    870     }
    871 
    872     /**
    873      * To be called from our Activity's onSaveInstanceState() to give us a chance
    874      * to stow our state away for later retrieval.
    875      *
    876      * @param bundle The Bundle passed in to onSaveInstanceState
    877      */
    878     public void writeStateToBundle(Bundle bundle) {
    879         if (hasSubject()) {
    880             bundle.putString("subject", mSubject.toString());
    881         }
    882 
    883         if (mMessageUri != null) {
    884             bundle.putParcelable("msg_uri", mMessageUri);
    885         } else if (hasText()) {
    886             bundle.putString("sms_body", mText.toString());
    887         }
    888     }
    889 
    890     /**
    891      * To be called from our Activity's onCreate() if the activity manager
    892      * has given it a Bundle to reinflate
    893      * @param bundle The Bundle passed in to onCreate
    894      */
    895     public void readStateFromBundle(Bundle bundle) {
    896         if (bundle == null) {
    897             return;
    898         }
    899 
    900         String subject = bundle.getString("subject");
    901         setSubject(subject, false);
    902 
    903         Uri uri = (Uri)bundle.getParcelable("msg_uri");
    904         if (uri != null) {
    905             loadFromUri(uri);
    906             return;
    907         } else {
    908             String body = bundle.getString("sms_body");
    909             mText = body;
    910         }
    911     }
    912 
    913     /**
    914      * Update the temporary list of recipients, used when setting up a
    915      * new conversation.  Will be converted to a ContactList on any
    916      * save event (send, save draft, etc.)
    917      */
    918     public void setWorkingRecipients(List<String> numbers) {
    919         mWorkingRecipients = numbers;
    920         String s = null;
    921         if (numbers != null) {
    922             int size = numbers.size();
    923             switch (size) {
    924             case 1:
    925                 s = numbers.get(0);
    926                 break;
    927             case 0:
    928                 s = "empty";
    929                 break;
    930             default:
    931                 s = "{...} len=" + size;
    932             }
    933         }
    934     }
    935 
    936     private void dumpWorkingRecipients() {
    937         Log.i(TAG, "-- mWorkingRecipients:");
    938 
    939         if (mWorkingRecipients != null) {
    940             int count = mWorkingRecipients.size();
    941             for (int i=0; i<count; i++) {
    942                 Log.i(TAG, "   [" + i + "] " + mWorkingRecipients.get(i));
    943             }
    944             Log.i(TAG, "");
    945         }
    946     }
    947 
    948     public void dump() {
    949         Log.i(TAG, "WorkingMessage:");
    950         dumpWorkingRecipients();
    951         if (mConversation != null) {
    952             Log.i(TAG, "mConversation: " + mConversation.toString());
    953         }
    954     }
    955 
    956     /**
    957      * Set the conversation associated with this message.
    958      */
    959     public void setConversation(Conversation conv) {
    960         if (DEBUG) LogTag.debug("setConversation %s -> %s", mConversation, conv);
    961 
    962         mConversation = conv;
    963 
    964         // Convert to MMS if there are any email addresses in the recipient list.
    965         setHasEmail(conv.getRecipients().containsEmail(), false);
    966     }
    967 
    968     public Conversation getConversation() {
    969         return mConversation;
    970     }
    971 
    972     /**
    973      * Hint whether or not this message will be delivered to an
    974      * an email address.
    975      */
    976     public void setHasEmail(boolean hasEmail, boolean notify) {
    977         if (MmsConfig.getEmailGateway() != null) {
    978             updateState(RECIPIENTS_REQUIRE_MMS, false, notify);
    979         } else {
    980             updateState(RECIPIENTS_REQUIRE_MMS, hasEmail, notify);
    981         }
    982     }
    983 
    984     /**
    985      * Returns true if this message would require MMS to send.
    986      */
    987     public boolean requiresMms() {
    988         return (mMmsState > 0);
    989     }
    990 
    991     /**
    992      * Set whether or not we want to send this message via MMS in order to
    993      * avoid sending an excessive number of concatenated SMS messages.
    994      * @param: mmsRequired is the value for the LENGTH_REQUIRES_MMS bit.
    995      * @param: notify Whether or not to notify the user.
    996     */
    997     public void setLengthRequiresMms(boolean mmsRequired, boolean notify) {
    998         updateState(LENGTH_REQUIRES_MMS, mmsRequired, notify);
    999     }
   1000 
   1001     private static String stateString(int state) {
   1002         if (state == 0)
   1003             return "<none>";
   1004 
   1005         StringBuilder sb = new StringBuilder();
   1006         if ((state & RECIPIENTS_REQUIRE_MMS) > 0)
   1007             sb.append("RECIPIENTS_REQUIRE_MMS | ");
   1008         if ((state & HAS_SUBJECT) > 0)
   1009             sb.append("HAS_SUBJECT | ");
   1010         if ((state & HAS_ATTACHMENT) > 0)
   1011             sb.append("HAS_ATTACHMENT | ");
   1012         if ((state & LENGTH_REQUIRES_MMS) > 0)
   1013             sb.append("LENGTH_REQUIRES_MMS | ");
   1014         if ((state & FORCE_MMS) > 0)
   1015             sb.append("FORCE_MMS | ");
   1016 
   1017         sb.delete(sb.length() - 3, sb.length());
   1018         return sb.toString();
   1019     }
   1020 
   1021     /**
   1022      * Sets the current state of our various "MMS required" bits.
   1023      *
   1024      * @param state The bit to change, such as {@link HAS_ATTACHMENT}
   1025      * @param on If true, set it; if false, clear it
   1026      * @param notify Whether or not to notify the user
   1027      */
   1028     private void updateState(int state, boolean on, boolean notify) {
   1029         if (!sMmsEnabled) {
   1030             // If Mms isn't enabled, the rest of the Messaging UI should not be using any
   1031             // feature that would cause us to to turn on any Mms flag and show the
   1032             // "Converting to multimedia..." message.
   1033             return;
   1034         }
   1035         int oldState = mMmsState;
   1036         if (on) {
   1037             mMmsState |= state;
   1038         } else {
   1039             mMmsState &= ~state;
   1040         }
   1041 
   1042         // If we are clearing the last bit that is not FORCE_MMS,
   1043         // expire the FORCE_MMS bit.
   1044         if (mMmsState == FORCE_MMS && ((oldState & ~FORCE_MMS) > 0)) {
   1045             mMmsState = 0;
   1046         }
   1047 
   1048         // Notify the listener if we are moving from SMS to MMS
   1049         // or vice versa.
   1050         if (notify) {
   1051             if (oldState == 0 && mMmsState != 0) {
   1052                 mStatusListener.onProtocolChanged(true);
   1053             } else if (oldState != 0 && mMmsState == 0) {
   1054                 mStatusListener.onProtocolChanged(false);
   1055             }
   1056         }
   1057 
   1058         if (oldState != mMmsState) {
   1059             if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) LogTag.debug("updateState: %s%s = %s",
   1060                     on ? "+" : "-",
   1061                     stateString(state), stateString(mMmsState));
   1062         }
   1063     }
   1064 
   1065     /**
   1066      * Send this message over the network.  Will call back with onMessageSent() once
   1067      * it has been dispatched to the telephony stack.  This WorkingMessage object is
   1068      * no longer useful after this method has been called.
   1069      *
   1070      * @throws ContentRestrictionException if sending an MMS and uaProfUrl is not defined
   1071      * in mms_config.xml.
   1072      */
   1073     public void send(final String recipientsInUI) {
   1074         if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
   1075             LogTag.debug("send");
   1076         }
   1077         long origThreadId = mConversation.getThreadId();
   1078 
   1079         removeSubjectIfEmpty(true /* notify */);
   1080 
   1081         // Get ready to write to disk.
   1082         prepareForSave(true /* notify */);
   1083 
   1084         // We need the recipient list for both SMS and MMS.
   1085         final Conversation conv = mConversation;
   1086         String msgTxt = mText.toString();
   1087 
   1088         if (requiresMms() || addressContainsEmailToMms(conv, msgTxt)) {
   1089             // uaProfUrl setting in mms_config.xml must be present to send an MMS.
   1090             // However, SMS service will still work in the absence of a uaProfUrl address.
   1091             if (MmsConfig.getUaProfUrl() == null) {
   1092                 String err = "WorkingMessage.send MMS sending failure. mms_config.xml is " +
   1093                         "missing uaProfUrl setting.  uaProfUrl is required for MMS service, " +
   1094                         "but can be absent for SMS.";
   1095                 RuntimeException ex = new ContentRestrictionException(err);
   1096                 Log.e(TAG, err, ex);
   1097                 // now, let's just crash.
   1098                 throw ex;
   1099             }
   1100 
   1101             // Make local copies of the bits we need for sending a message,
   1102             // because we will be doing it off of the main thread, which will
   1103             // immediately continue on to resetting some of this state.
   1104             final Uri mmsUri = mMessageUri;
   1105             final PduPersister persister = PduPersister.getPduPersister(mActivity);
   1106 
   1107             final SlideshowModel slideshow = mSlideshow;
   1108             final CharSequence subject = mSubject;
   1109 
   1110             // Do the dirty work of sending the message off of the main UI thread.
   1111             new Thread(new Runnable() {
   1112                 public void run() {
   1113                     final SendReq sendReq = makeSendReq(conv, subject);
   1114 
   1115                     // Make sure the text in slide 0 is no longer holding onto a reference to
   1116                     // the text in the message text box.
   1117                     slideshow.prepareForSend();
   1118                     sendMmsWorker(conv, mmsUri, persister, slideshow, sendReq);
   1119 
   1120                     updateSendStats(conv);
   1121                 }
   1122             }).start();
   1123         } else {
   1124             // Same rules apply as above.
   1125             final String msgText = mText.toString();
   1126             new Thread(new Runnable() {
   1127                 public void run() {
   1128                     preSendSmsWorker(conv, msgText, recipientsInUI);
   1129 
   1130                     updateSendStats(conv);
   1131                 }
   1132             }).start();
   1133         }
   1134 
   1135         // update the Recipient cache with the new to address, if it's different
   1136         RecipientIdCache.updateNumbers(conv.getThreadId(), conv.getRecipients());
   1137 
   1138         // Mark the message as discarded because it is "off the market" after being sent.
   1139         mDiscarded = true;
   1140     }
   1141 
   1142     // Be sure to only call this on a background thread.
   1143     private void updateSendStats(final Conversation conv) {
   1144         String[] dests = conv.getRecipients().getNumbers();
   1145         final ArrayList<String> phoneNumbers = new ArrayList<String>(Arrays.asList(dests));
   1146 
   1147         DataUsageStatUpdater updater = new DataUsageStatUpdater(mActivity);
   1148         updater.updateWithPhoneNumber(phoneNumbers);
   1149     }
   1150 
   1151     private boolean addressContainsEmailToMms(Conversation conv, String text) {
   1152         if (MmsConfig.getEmailGateway() != null) {
   1153             String[] dests = conv.getRecipients().getNumbers();
   1154             int length = dests.length;
   1155             for (int i = 0; i < length; i++) {
   1156                 if (Mms.isEmailAddress(dests[i]) || MessageUtils.isAlias(dests[i])) {
   1157                     String mtext = dests[i] + " " + text;
   1158                     int[] params = SmsMessage.calculateLength(mtext, false);
   1159                     if (params[0] > 1) {
   1160                         updateState(RECIPIENTS_REQUIRE_MMS, true, true);
   1161                         ensureSlideshow();
   1162                         syncTextToSlideshow();
   1163                         return true;
   1164                     }
   1165                 }
   1166             }
   1167         }
   1168         return false;
   1169     }
   1170 
   1171     // Message sending stuff
   1172 
   1173     private void preSendSmsWorker(Conversation conv, String msgText, String recipientsInUI) {
   1174         // If user tries to send the message, it's a signal the inputted text is what they wanted.
   1175         UserHappinessSignals.userAcceptedImeText(mActivity);
   1176 
   1177         mStatusListener.onPreMessageSent();
   1178 
   1179         long origThreadId = conv.getThreadId();
   1180 
   1181         // Make sure we are still using the correct thread ID for our recipient set.
   1182         long threadId = conv.ensureThreadId();
   1183 
   1184         String semiSepRecipients = conv.getRecipients().serialize();
   1185 
   1186         // recipientsInUI can be empty when the user types in a number and hits send
   1187         if (LogTag.SEVERE_WARNING && ((origThreadId != 0 && origThreadId != threadId) ||
   1188                (!semiSepRecipients.equals(recipientsInUI) && !TextUtils.isEmpty(recipientsInUI)))) {
   1189             String msg = origThreadId != 0 && origThreadId != threadId ?
   1190                     "WorkingMessage.preSendSmsWorker threadId changed or " +
   1191                     "recipients changed. origThreadId: " +
   1192                     origThreadId + " new threadId: " + threadId +
   1193                     " also mConversation.getThreadId(): " +
   1194                     mConversation.getThreadId()
   1195                 :
   1196                     "Recipients in window: \"" +
   1197                     recipientsInUI + "\" differ from recipients from conv: \"" +
   1198                     semiSepRecipients + "\"";
   1199 
   1200             LogTag.warnPossibleRecipientMismatch(msg, mActivity);
   1201         }
   1202 
   1203         // just do a regular send. We're already on a non-ui thread so no need to fire
   1204         // off another thread to do this work.
   1205         sendSmsWorker(msgText, semiSepRecipients, threadId);
   1206 
   1207         // Be paranoid and clean any draft SMS up.
   1208         deleteDraftSmsMessage(threadId);
   1209     }
   1210 
   1211     private void sendSmsWorker(String msgText, String semiSepRecipients, long threadId) {
   1212         String[] dests = TextUtils.split(semiSepRecipients, ";");
   1213         if (LogTag.VERBOSE || Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
   1214             Log.d(LogTag.TRANSACTION, "sendSmsWorker sending message: recipients=" +
   1215                     semiSepRecipients + ", threadId=" + threadId);
   1216         }
   1217         MessageSender sender = new SmsMessageSender(mActivity, dests, msgText, threadId);
   1218         try {
   1219             sender.sendMessage(threadId);
   1220 
   1221             // Make sure this thread isn't over the limits in message count
   1222             Recycler.getSmsRecycler().deleteOldMessagesByThreadId(mActivity, threadId);
   1223         } catch (Exception e) {
   1224             Log.e(TAG, "Failed to send SMS message, threadId=" + threadId, e);
   1225         }
   1226 
   1227         mStatusListener.onMessageSent();
   1228     }
   1229 
   1230     private void sendMmsWorker(Conversation conv, Uri mmsUri, PduPersister persister,
   1231                                SlideshowModel slideshow, SendReq sendReq) {
   1232         // If user tries to send the message, it's a signal the inputted text is what they wanted.
   1233         UserHappinessSignals.userAcceptedImeText(mActivity);
   1234 
   1235         // First make sure we don't have too many outstanding unsent message.
   1236         Cursor cursor = null;
   1237         try {
   1238             cursor = SqliteWrapper.query(mActivity, mContentResolver,
   1239                     Mms.Outbox.CONTENT_URI, MMS_OUTBOX_PROJECTION, null, null, null);
   1240             if (cursor != null) {
   1241                 long maxMessageSize = MmsConfig.getMaxSizeScaleForPendingMmsAllowed() *
   1242                     MmsConfig.getMaxMessageSize();
   1243                 long totalPendingSize = 0;
   1244                 while (cursor.moveToNext()) {
   1245                     totalPendingSize += cursor.getLong(MMS_MESSAGE_SIZE_INDEX);
   1246                 }
   1247                 if (totalPendingSize >= maxMessageSize) {
   1248                     unDiscard();    // it wasn't successfully sent. Allow it to be saved as a draft.
   1249                     mStatusListener.onMaxPendingMessagesReached();
   1250                     return;
   1251                 }
   1252             }
   1253         } finally {
   1254             if (cursor != null) {
   1255                 cursor.close();
   1256             }
   1257         }
   1258         mStatusListener.onPreMessageSent();
   1259         long threadId = 0;
   1260 
   1261         try {
   1262             DraftCache.getInstance().setSavingDraft(true);
   1263 
   1264             // Make sure we are still using the correct thread ID for our
   1265             // recipient set.
   1266             threadId = conv.ensureThreadId();
   1267 
   1268             if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1269                 LogTag.debug("sendMmsWorker: update draft MMS message " + mmsUri);
   1270             }
   1271 
   1272             // One last check to verify the address of the recipient.
   1273             String[] dests = conv.getRecipients().getNumbers(true /* scrub for MMS address */);
   1274             if (dests.length == 1) {
   1275                 // verify the single address matches what's in the database. If we get a different
   1276                 // address back, jam the new value back into the SendReq.
   1277                 String newAddress =
   1278                     Conversation.verifySingleRecipient(mActivity, conv.getThreadId(), dests[0]);
   1279 
   1280                 if (!newAddress.equals(dests[0])) {
   1281                     dests[0] = newAddress;
   1282                     EncodedStringValue[] encodedNumbers = EncodedStringValue.encodeStrings(dests);
   1283                     if (encodedNumbers != null) {
   1284                         sendReq.setTo(encodedNumbers);
   1285                     }
   1286                 }
   1287             }
   1288 
   1289             if (mmsUri == null) {
   1290                 // Create a new MMS message if one hasn't been made yet.
   1291                 mmsUri = createDraftMmsMessage(persister, sendReq, slideshow);
   1292             } else {
   1293                 // Otherwise, sync the MMS message in progress to disk.
   1294                 updateDraftMmsMessage(mmsUri, persister, slideshow, sendReq);
   1295             }
   1296             // Be paranoid and clean any draft SMS up.
   1297             deleteDraftSmsMessage(threadId);
   1298         } finally {
   1299             DraftCache.getInstance().setSavingDraft(false);
   1300         }
   1301 
   1302         // Resize all the resizeable attachments (e.g. pictures) to fit
   1303         // in the remaining space in the slideshow.
   1304         int error = 0;
   1305         try {
   1306             slideshow.finalResize(mmsUri);
   1307         } catch (ExceedMessageSizeException e1) {
   1308             error = MESSAGE_SIZE_EXCEEDED;
   1309         } catch (MmsException e1) {
   1310             error = UNKNOWN_ERROR;
   1311         }
   1312         if (error != 0) {
   1313             markMmsMessageWithError(mmsUri);
   1314             mStatusListener.onAttachmentError(error);
   1315             return;
   1316         }
   1317 
   1318         MessageSender sender = new MmsMessageSender(mActivity, mmsUri,
   1319                 slideshow.getCurrentMessageSize());
   1320         try {
   1321             if (!sender.sendMessage(threadId)) {
   1322                 // The message was sent through SMS protocol, we should
   1323                 // delete the copy which was previously saved in MMS drafts.
   1324                 SqliteWrapper.delete(mActivity, mContentResolver, mmsUri, null, null);
   1325             }
   1326 
   1327             // Make sure this thread isn't over the limits in message count
   1328             Recycler.getMmsRecycler().deleteOldMessagesByThreadId(mActivity, threadId);
   1329         } catch (Exception e) {
   1330             Log.e(TAG, "Failed to send message: " + mmsUri + ", threadId=" + threadId, e);
   1331         }
   1332 
   1333         mStatusListener.onMessageSent();
   1334     }
   1335 
   1336     private void markMmsMessageWithError(Uri mmsUri) {
   1337         try {
   1338             PduPersister p = PduPersister.getPduPersister(mActivity);
   1339             // Move the message into MMS Outbox. A trigger will create an entry in
   1340             // the "pending_msgs" table.
   1341             p.move(mmsUri, Mms.Outbox.CONTENT_URI);
   1342 
   1343             // Now update the pending_msgs table with an error for that new item.
   1344             ContentValues values = new ContentValues(1);
   1345             values.put(PendingMessages.ERROR_TYPE, MmsSms.ERR_TYPE_GENERIC_PERMANENT);
   1346             long msgId = ContentUris.parseId(mmsUri);
   1347             SqliteWrapper.update(mActivity, mContentResolver,
   1348                     PendingMessages.CONTENT_URI,
   1349                     values, PendingMessages.MSG_ID + "=" + msgId, null);
   1350         } catch (MmsException e) {
   1351             // Not much we can do here. If the p.move throws an exception, we'll just
   1352             // leave the message in the draft box.
   1353             Log.e(TAG, "Failed to move message to outbox and mark as error: " + mmsUri, e);
   1354         }
   1355     }
   1356 
   1357     // Draft message stuff
   1358 
   1359     private static final String[] MMS_DRAFT_PROJECTION = {
   1360         Mms._ID,                // 0
   1361         Mms.SUBJECT,            // 1
   1362         Mms.SUBJECT_CHARSET     // 2
   1363     };
   1364 
   1365     private static final int MMS_ID_INDEX         = 0;
   1366     private static final int MMS_SUBJECT_INDEX    = 1;
   1367     private static final int MMS_SUBJECT_CS_INDEX = 2;
   1368 
   1369     private static Uri readDraftMmsMessage(Context context, Conversation conv, StringBuilder sb) {
   1370         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1371             LogTag.debug("readDraftMmsMessage conv: " + conv);
   1372         }
   1373         Cursor cursor;
   1374         ContentResolver cr = context.getContentResolver();
   1375 
   1376         final String selection = Mms.THREAD_ID + " = " + conv.getThreadId();
   1377         cursor = SqliteWrapper.query(context, cr,
   1378                 Mms.Draft.CONTENT_URI, MMS_DRAFT_PROJECTION,
   1379                 selection, null, null);
   1380 
   1381         Uri uri;
   1382         try {
   1383             if (cursor.moveToFirst()) {
   1384                 uri = ContentUris.withAppendedId(Mms.Draft.CONTENT_URI,
   1385                         cursor.getLong(MMS_ID_INDEX));
   1386                 String subject = MessageUtils.extractEncStrFromCursor(cursor, MMS_SUBJECT_INDEX,
   1387                         MMS_SUBJECT_CS_INDEX);
   1388                 if (subject != null) {
   1389                     sb.append(subject);
   1390                 }
   1391                 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1392                     LogTag.debug("readDraftMmsMessage uri: ", uri);
   1393                 }
   1394                 return uri;
   1395             }
   1396         } finally {
   1397             cursor.close();
   1398         }
   1399 
   1400         return null;
   1401     }
   1402 
   1403     /**
   1404      * makeSendReq should always return a non-null SendReq, whether the dest addresses are
   1405      * valid or not.
   1406      */
   1407     private static SendReq makeSendReq(Conversation conv, CharSequence subject) {
   1408         String[] dests = conv.getRecipients().getNumbers(true /* scrub for MMS address */);
   1409 
   1410         SendReq req = new SendReq();
   1411         EncodedStringValue[] encodedNumbers = EncodedStringValue.encodeStrings(dests);
   1412         if (encodedNumbers != null) {
   1413             req.setTo(encodedNumbers);
   1414         }
   1415 
   1416         if (!TextUtils.isEmpty(subject)) {
   1417             req.setSubject(new EncodedStringValue(subject.toString()));
   1418         }
   1419 
   1420         req.setDate(System.currentTimeMillis() / 1000L);
   1421 
   1422         return req;
   1423     }
   1424 
   1425     private static Uri createDraftMmsMessage(PduPersister persister, SendReq sendReq,
   1426             SlideshowModel slideshow) {
   1427         try {
   1428             PduBody pb = slideshow.toPduBody();
   1429             sendReq.setBody(pb);
   1430             Uri res = persister.persist(sendReq, Mms.Draft.CONTENT_URI);
   1431             slideshow.sync(pb);
   1432             return res;
   1433         } catch (MmsException e) {
   1434             return null;
   1435         }
   1436     }
   1437 
   1438     private void asyncUpdateDraftMmsMessage(final Conversation conv, final boolean isStopping) {
   1439         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1440             LogTag.debug("asyncUpdateDraftMmsMessage conv=%s mMessageUri=%s", conv, mMessageUri);
   1441         }
   1442 
   1443         new Thread(new Runnable() {
   1444             public void run() {
   1445                 try {
   1446                     DraftCache.getInstance().setSavingDraft(true);
   1447                     final PduPersister persister = PduPersister.getPduPersister(mActivity);
   1448                     final SendReq sendReq = makeSendReq(conv, mSubject);
   1449 
   1450                     if (mMessageUri == null) {
   1451                         mMessageUri = createDraftMmsMessage(persister, sendReq, mSlideshow);
   1452                     } else {
   1453                         updateDraftMmsMessage(mMessageUri, persister, mSlideshow, sendReq);
   1454                     }
   1455                     if (isStopping && conv.getMessageCount() == 0) {
   1456                         // createDraftMmsMessage can create the new thread in the threads table (the
   1457                         // call to createDraftMmsDraftMessage calls PduPersister.persist() which
   1458                         // can call Threads.getOrCreateThreadId()). Meanwhile, when the user goes
   1459                         // back to ConversationList while we're saving a draft from CMA's.onStop,
   1460                         // ConversationList will delete all threads from the thread table that
   1461                         // don't have associated sms or pdu entries. In case our thread got deleted,
   1462                         // well call clearThreadId() so ensureThreadId will query the db for the new
   1463                         // thread.
   1464                         conv.clearThreadId();   // force us to get the updated thread id
   1465                     }
   1466                     conv.ensureThreadId();
   1467                     conv.setDraftState(true);
   1468                     if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1469                         LogTag.debug("asyncUpdateDraftMmsMessage conv: " + conv +
   1470                                 " uri: " + mMessageUri);
   1471                     }
   1472 
   1473                     // Be paranoid and delete any SMS drafts that might be lying around. Must do
   1474                     // this after ensureThreadId so conv has the correct thread id.
   1475                     asyncDeleteDraftSmsMessage(conv);
   1476                 } finally {
   1477                     DraftCache.getInstance().setSavingDraft(false);
   1478                 }
   1479             }
   1480         }).start();
   1481     }
   1482 
   1483     private static void updateDraftMmsMessage(Uri uri, PduPersister persister,
   1484             SlideshowModel slideshow, SendReq sendReq) {
   1485         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1486             LogTag.debug("updateDraftMmsMessage uri=%s", uri);
   1487         }
   1488         if (uri == null) {
   1489             Log.e(TAG, "updateDraftMmsMessage null uri");
   1490             return;
   1491         }
   1492         persister.updateHeaders(uri, sendReq);
   1493         final PduBody pb = slideshow.toPduBody();
   1494 
   1495         try {
   1496             persister.updateParts(uri, pb);
   1497         } catch (MmsException e) {
   1498             Log.e(TAG, "updateDraftMmsMessage: cannot update message " + uri);
   1499         }
   1500 
   1501         slideshow.sync(pb);
   1502     }
   1503 
   1504     private static final String SMS_DRAFT_WHERE = Sms.TYPE + "=" + Sms.MESSAGE_TYPE_DRAFT;
   1505     private static final String[] SMS_BODY_PROJECTION = { Sms.BODY };
   1506     private static final int SMS_BODY_INDEX = 0;
   1507 
   1508     /**
   1509      * Reads a draft message for the given thread ID from the database,
   1510      * if there is one, deletes it from the database, and returns it.
   1511      * @return The draft message or an empty string.
   1512      */
   1513     private String readDraftSmsMessage(Conversation conv) {
   1514         long thread_id = conv.getThreadId();
   1515         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1516             Log.d(TAG, "readDraftSmsMessage conv: " + conv);
   1517         }
   1518         // If it's an invalid thread or we know there's no draft, don't bother.
   1519         if (thread_id <= 0 || !conv.hasDraft()) {
   1520             return "";
   1521         }
   1522 
   1523         Uri thread_uri = ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, thread_id);
   1524         String body = "";
   1525 
   1526         Cursor c = SqliteWrapper.query(mActivity, mContentResolver,
   1527                         thread_uri, SMS_BODY_PROJECTION, SMS_DRAFT_WHERE, null, null);
   1528         boolean haveDraft = false;
   1529         if (c != null) {
   1530             try {
   1531                 if (c.moveToFirst()) {
   1532                     body = c.getString(SMS_BODY_INDEX);
   1533                     haveDraft = true;
   1534                 }
   1535             } finally {
   1536                 c.close();
   1537             }
   1538         }
   1539 
   1540         // We found a draft, and if there are no messages in the conversation,
   1541         // that means we deleted the thread, too. Must reset the thread id
   1542         // so we'll eventually create a new thread.
   1543         if (haveDraft && conv.getMessageCount() == 0) {
   1544             asyncDeleteDraftSmsMessage(conv);
   1545 
   1546             // Clean out drafts for this thread -- if the recipient set changes,
   1547             // we will lose track of the original draft and be unable to delete
   1548             // it later.  The message will be re-saved if necessary upon exit of
   1549             // the activity.
   1550             clearConversation(conv, true);
   1551         }
   1552         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1553             LogTag.debug("readDraftSmsMessage haveDraft: ", !TextUtils.isEmpty(body));
   1554         }
   1555 
   1556         return body;
   1557     }
   1558 
   1559     public void clearConversation(final Conversation conv, boolean resetThreadId) {
   1560         if (resetThreadId && conv.getMessageCount() == 0) {
   1561             if (DEBUG) LogTag.debug("clearConversation calling clearThreadId");
   1562             conv.clearThreadId();
   1563         }
   1564 
   1565         conv.setDraftState(false);
   1566     }
   1567 
   1568     private void asyncUpdateDraftSmsMessage(final Conversation conv, final String contents) {
   1569         new Thread(new Runnable() {
   1570             public void run() {
   1571                 try {
   1572                     DraftCache.getInstance().setSavingDraft(true);
   1573                     long threadId = conv.ensureThreadId();
   1574                     conv.setDraftState(true);
   1575                     updateDraftSmsMessage(conv, contents);
   1576                 } finally {
   1577                     DraftCache.getInstance().setSavingDraft(false);
   1578                 }
   1579             }
   1580         }).start();
   1581     }
   1582 
   1583     private void updateDraftSmsMessage(final Conversation conv, String contents) {
   1584         final long threadId = conv.getThreadId();
   1585         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1586             LogTag.debug("updateDraftSmsMessage tid=%d, contents=\"%s\"", threadId, contents);
   1587         }
   1588 
   1589         // If we don't have a valid thread, there's nothing to do.
   1590         if (threadId <= 0) {
   1591             return;
   1592         }
   1593 
   1594         ContentValues values = new ContentValues(3);
   1595         values.put(Sms.THREAD_ID, threadId);
   1596         values.put(Sms.BODY, contents);
   1597         values.put(Sms.TYPE, Sms.MESSAGE_TYPE_DRAFT);
   1598         SqliteWrapper.insert(mActivity, mContentResolver, Sms.CONTENT_URI, values);
   1599         asyncDeleteDraftMmsMessage(conv);
   1600     }
   1601 
   1602     private void asyncDelete(final Uri uri, final String selection, final String[] selectionArgs) {
   1603         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
   1604             LogTag.debug("asyncDelete %s where %s", uri, selection);
   1605         }
   1606         new Thread(new Runnable() {
   1607             public void run() {
   1608                 SqliteWrapper.delete(mActivity, mContentResolver, uri, selection, selectionArgs);
   1609             }
   1610         }).start();
   1611     }
   1612 
   1613     public void asyncDeleteDraftSmsMessage(Conversation conv) {
   1614         mHasSmsDraft = false;
   1615 
   1616         final long threadId = conv.getThreadId();
   1617         if (threadId > 0) {
   1618             asyncDelete(ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, threadId),
   1619                 SMS_DRAFT_WHERE, null);
   1620         }
   1621     }
   1622 
   1623     private void deleteDraftSmsMessage(long threadId) {
   1624         SqliteWrapper.delete(mActivity, mContentResolver,
   1625                 ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, threadId),
   1626                 SMS_DRAFT_WHERE, null);
   1627     }
   1628 
   1629     private void asyncDeleteDraftMmsMessage(Conversation conv) {
   1630         mHasMmsDraft = false;
   1631 
   1632         final long threadId = conv.getThreadId();
   1633         if (threadId > 0) {
   1634             final String where = Mms.THREAD_ID + " = " + threadId;
   1635             asyncDelete(Mms.Draft.CONTENT_URI, where, null);
   1636         }
   1637     }
   1638 }
   1639