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