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