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