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