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