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