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