WorkingMessage.java revision 9abae049669b8525b28ec4f4adb2e14372b4d931
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        SlideshowEditor slideShowEditor = new SlideshowEditor(mActivity, mSlideshow);
381
382        // Special case for deleting a slideshow. When ComposeMessageActivity gets told to
383        // remove an attachment (search for AttachmentEditor.MSG_REMOVE_ATTACHMENT), it calls
384        // this function setAttachment with a type of TEXT and a null uri. Basically, it's turning
385        // the working message from an MMS back to a simple SMS. The various attachment types
386        // use slide[0] as a special case. The call to ensureSlideshow below makes sure there's
387        // a slide zero. In the case of an already attached slideshow, ensureSlideshow will do
388        // nothing and the slideshow will remain such that if a user adds a slideshow again, they'll
389        // see their old slideshow they previously deleted. Here we really delete the slideshow.
390        if (type == TEXT && mAttachmentType == SLIDESHOW && mSlideshow != null && dataUri == null
391                && !append) {
392            slideShowEditor.removeAllSlides();
393        }
394
395        // Make sure mSlideshow is set up and has a slide.
396        ensureSlideshow();      // mSlideshow can be null before this call, won't be afterwards
397        slideShowEditor.setSlideshow(mSlideshow);
398
399        // Change the attachment
400        result = append ? appendMedia(type, dataUri, slideShowEditor)
401                : changeMedia(type, dataUri, slideShowEditor);
402
403        // If we were successful, update mAttachmentType and notify
404        // the listener than there was a change.
405        if (result == OK) {
406            mAttachmentType = type;
407        }
408        correctAttachmentState();
409
410        mStatusListener.onAttachmentChanged();  // have to call whether succeeded or failed,
411                                                // because a replace that fails, removes the slide
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        return result;
437    }
438
439    /**
440     * Returns true if this message contains anything worth saving.
441     */
442    public boolean isWorthSaving() {
443        // If it actually contains anything, it's of course not empty.
444        if (hasText() || hasSubject() || hasAttachment() || hasSlideshow()) {
445            return true;
446        }
447
448        // When saveAsMms() has been called, we set FORCE_MMS to represent
449        // sort of an "invisible attachment" so that the message isn't thrown
450        // away when we are shipping it off to other activities.
451        if (isFakeMmsForDraft()) {
452            return true;
453        }
454
455        return false;
456    }
457
458    /**
459     * Returns true if FORCE_MMS is set.
460     * When saveAsMms() has been called, we set FORCE_MMS to represent
461     * sort of an "invisible attachment" so that the message isn't thrown
462     * away when we are shipping it off to other activities.
463     */
464    public boolean isFakeMmsForDraft() {
465        return (mMmsState & FORCE_MMS) > 0;
466    }
467
468    /**
469     * Makes sure mSlideshow is set up.
470     */
471    private void ensureSlideshow() {
472        if (mSlideshow != null) {
473            return;
474        }
475
476        SlideshowModel slideshow = SlideshowModel.createNew(mActivity);
477        SlideModel slide = new SlideModel(slideshow);
478        slideshow.add(slide);
479
480        mSlideshow = slideshow;
481    }
482
483    /**
484     * Change the message's attachment to the data in the specified Uri.
485     * Used only for single-slide ("attachment mode") messages. If the attachment fails to
486     * attach, restore the slide to its original state.
487     */
488    private int changeMedia(int type, Uri uri, SlideshowEditor slideShowEditor) {
489        SlideModel originalSlide = mSlideshow.get(0);
490        if (originalSlide != null) {
491            slideShowEditor.removeSlide(0);     // remove the original slide
492        }
493        slideShowEditor.addNewSlide(0);
494        SlideModel slide = mSlideshow.get(0);   // get the new empty slide
495        int result = OK;
496
497        if (slide == null) {
498            Log.w(LogTag.TAG, "[WorkingMessage] changeMedia: no slides!");
499            return result;
500        }
501
502        // Clear the attachment type since we removed all the attachments. If this isn't cleared
503        // and the slide.add fails (for instance, a selected video could be too big), we'll be
504        // left in a state where we think we have an attachment, but it's been removed from the
505        // slide.
506        mAttachmentType = TEXT;
507
508        // If we're changing to text, just bail out.
509        if (type == TEXT) {
510            return result;
511        }
512
513        result = internalChangeMedia(type, uri, 0, slideShowEditor);
514        if (result != OK) {
515            slideShowEditor.removeSlide(0);             // remove the failed slide
516            if (originalSlide != null) {
517                slideShowEditor.addSlide(0, originalSlide); // restore the original slide.
518            }
519        }
520        return result;
521    }
522
523    /**
524     * Add the message's attachment to the data in the specified Uri to a new slide.
525     */
526    private int appendMedia(int type, Uri uri, SlideshowEditor slideShowEditor) {
527        int result = OK;
528
529        // If we're changing to text, just bail out.
530        if (type == TEXT) {
531            return result;
532        }
533
534        // The first time this method is called, mSlideshow.size() is going to be
535        // one (a newly initialized slideshow has one empty slide). The first time we
536        // attach the picture/video to that first empty slide. From then on when this
537        // function is called, we've got to create a new slide and add the picture/video
538        // to that new slide.
539        boolean addNewSlide = true;
540        if (mSlideshow.size() == 1 && !mSlideshow.isSimple()) {
541            addNewSlide = false;
542        }
543        if (addNewSlide) {
544            if (!slideShowEditor.addNewSlide()) {
545                return result;
546            }
547        }
548        int slideNum = mSlideshow.size() - 1;
549        result = internalChangeMedia(type, uri, slideNum, slideShowEditor);
550        if (result != OK) {
551            // We added a new slide and what we attempted to insert on the slide failed.
552            // Delete that slide, otherwise we could end up with a bunch of blank slides.
553            // It's ok that we're removing the slide even if we didn't add it (because it was
554            // the first default slide). If adding the first slide fails, we want to remove it.
555            slideShowEditor.removeSlide(slideNum);
556        }
557        return result;
558    }
559
560    private int internalChangeMedia(int type, Uri uri, int slideNum,
561            SlideshowEditor slideShowEditor) {
562        int result = OK;
563        try {
564            if (type == IMAGE) {
565                slideShowEditor.changeImage(slideNum, uri);
566            } else if (type == VIDEO) {
567                slideShowEditor.changeVideo(slideNum, uri);
568            } else if (type == AUDIO) {
569                slideShowEditor.changeAudio(slideNum, uri);
570            } else {
571                result = UNSUPPORTED_TYPE;
572            }
573        } catch (MmsException e) {
574            result = UNKNOWN_ERROR;
575        } catch (UnsupportContentTypeException e) {
576            result = UNSUPPORTED_TYPE;
577        } catch (ExceedMessageSizeException e) {
578            result = MESSAGE_SIZE_EXCEEDED;
579        } catch (ResolutionException e) {
580            result = IMAGE_TOO_LARGE;
581        }
582        return result;
583    }
584
585    /**
586     * Returns true if the message has an attachment (including slideshows).
587     */
588    public boolean hasAttachment() {
589        return (mAttachmentType > TEXT);
590    }
591
592    /**
593     * Returns the slideshow associated with this message.
594     */
595    public SlideshowModel getSlideshow() {
596        return mSlideshow;
597    }
598
599    /**
600     * Returns true if the message has a real slideshow, as opposed to just
601     * one image attachment, for example.
602     */
603    public boolean hasSlideshow() {
604        return (mAttachmentType == SLIDESHOW);
605    }
606
607    /**
608     * Sets the MMS subject of the message.  Passing null indicates that there
609     * is no subject.  Passing "" will result in an empty subject being added
610     * to the message, possibly triggering a conversion to MMS.  This extra
611     * bit of state is needed to support ComposeMessageActivity converting to
612     * MMS when the user adds a subject.  An empty subject will be removed
613     * before saving to disk or sending, however.
614     */
615    public void setSubject(CharSequence s, boolean notify) {
616        mSubject = s;
617        updateState(HAS_SUBJECT, (s != null), notify);
618    }
619
620    /**
621     * Returns the MMS subject of the message.
622     */
623    public CharSequence getSubject() {
624        return mSubject;
625    }
626
627    /**
628     * Returns true if this message has an MMS subject. A subject has to be more than just
629     * whitespace.
630     * @return
631     */
632    public boolean hasSubject() {
633        return mSubject != null && TextUtils.getTrimmedLength(mSubject) > 0;
634    }
635
636    /**
637     * Moves the message text into the slideshow.  Should be called any time
638     * the message is about to be sent or written to disk.
639     */
640    private void syncTextToSlideshow() {
641        if (mSlideshow == null || mSlideshow.size() != 1)
642            return;
643
644        SlideModel slide = mSlideshow.get(0);
645        TextModel text;
646        if (!slide.hasText()) {
647            // Add a TextModel to slide 0 if one doesn't already exist
648            text = new TextModel(mActivity, ContentType.TEXT_PLAIN, "text_0.txt",
649                                           mSlideshow.getLayout().getTextRegion());
650            slide.add(text);
651        } else {
652            // Otherwise just reuse the existing one.
653            text = slide.getText();
654        }
655        text.setText(mText);
656    }
657
658    /**
659     * Sets the message text out of the slideshow.  Should be called any time
660     * a slideshow is loaded from disk.
661     */
662    private void syncTextFromSlideshow() {
663        // Don't sync text for real slideshows.
664        if (mSlideshow.size() != 1) {
665            return;
666        }
667
668        SlideModel slide = mSlideshow.get(0);
669        if (slide == null || !slide.hasText()) {
670            return;
671        }
672
673        mText = slide.getText().getText();
674    }
675
676    /**
677     * Removes the subject if it is empty, possibly converting back to SMS.
678     */
679    private void removeSubjectIfEmpty(boolean notify) {
680        if (!hasSubject()) {
681            setSubject(null, notify);
682        }
683    }
684
685    /**
686     * Gets internal message state ready for storage.  Should be called any
687     * time the message is about to be sent or written to disk.
688     */
689    private void prepareForSave(boolean notify) {
690        // Make sure our working set of recipients is resolved
691        // to first-class Contact objects before we save.
692        syncWorkingRecipients();
693
694        if (requiresMms()) {
695            ensureSlideshow();
696            syncTextToSlideshow();
697        }
698    }
699
700    /**
701     * Resolve the temporary working set of recipients to a ContactList.
702     */
703    public void syncWorkingRecipients() {
704        if (mWorkingRecipients != null) {
705            ContactList recipients = ContactList.getByNumbers(mWorkingRecipients, false);
706            mConversation.setRecipients(recipients);    // resets the threadId to zero
707            mWorkingRecipients = null;
708        }
709    }
710
711    public String getWorkingRecipients() {
712        // this function is used for DEBUG only
713        if (mWorkingRecipients == null) {
714            return null;
715        }
716        ContactList recipients = ContactList.getByNumbers(mWorkingRecipients, false);
717        return recipients.serialize();
718    }
719
720    // Call when we've returned from adding an attachment. We're no longer forcing the message
721    // into a Mms message. At this point we either have the goods to make the message a Mms
722    // or we don't. No longer fake it.
723    public void removeFakeMmsForDraft() {
724        updateState(FORCE_MMS, false, false);
725    }
726
727    /**
728     * Force the message to be saved as MMS and return the Uri of the message.
729     * Typically used when handing a message off to another activity.
730     */
731    public Uri saveAsMms(boolean notify) {
732        if (DEBUG) LogTag.debug("saveAsMms mConversation=%s", mConversation);
733
734        // If we have discarded the message, just bail out.
735        if (mDiscarded) {
736            LogTag.warn("saveAsMms mDiscarded: true mConversation: " + mConversation +
737                    " returning NULL uri and bailing");
738            return null;
739        }
740
741        // FORCE_MMS behaves as sort of an "invisible attachment", making
742        // the message seem non-empty (and thus not discarded).  This bit
743        // is sticky until the last other MMS bit is removed, at which
744        // point the message will fall back to SMS.
745        updateState(FORCE_MMS, true, notify);
746
747        // Collect our state to be written to disk.
748        prepareForSave(true /* notify */);
749
750        try {
751            // Make sure we are saving to the correct thread ID.
752            DraftCache.getInstance().setSavingDraft(true);
753            mConversation.ensureThreadId();
754            mConversation.setDraftState(true);
755
756            PduPersister persister = PduPersister.getPduPersister(mActivity);
757            SendReq sendReq = makeSendReq(mConversation, mSubject);
758
759            // If we don't already have a Uri lying around, make a new one.  If we do
760            // have one already, make sure it is synced to disk.
761            if (mMessageUri == null) {
762                mMessageUri = createDraftMmsMessage(persister, sendReq, mSlideshow);
763            } else {
764                updateDraftMmsMessage(mMessageUri, persister, mSlideshow, sendReq);
765            }
766            mHasMmsDraft = true;
767        } finally {
768            DraftCache.getInstance().setSavingDraft(false);
769        }
770        return mMessageUri;
771    }
772
773    /**
774     * Save this message as a draft in the conversation previously specified
775     * to {@link setConversation}.
776     */
777    public void saveDraft(final boolean isStopping) {
778        // If we have discarded the message, just bail out.
779        if (mDiscarded) {
780            LogTag.warn("saveDraft mDiscarded: true mConversation: " + mConversation +
781                " skipping saving draft and bailing");
782            return;
783        }
784
785        // Make sure setConversation was called.
786        if (mConversation == null) {
787            throw new IllegalStateException("saveDraft() called with no conversation");
788        }
789
790        if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
791            LogTag.debug("saveDraft for mConversation " + mConversation);
792        }
793
794        // Get ready to write to disk. But don't notify message status when saving draft
795        prepareForSave(false /* notify */);
796
797        if (requiresMms()) {
798            asyncUpdateDraftMmsMessage(mConversation, isStopping);
799            mHasMmsDraft = true;
800        } else {
801            String content = mText.toString();
802
803            // bug 2169583: don't bother creating a thread id only to delete the thread
804            // because the content is empty. When we delete the thread in updateDraftSmsMessage,
805            // we didn't nullify conv.mThreadId, causing a temperary situation where conv
806            // is holding onto a thread id that isn't in the database. If a new message arrives
807            // and takes that thread id (because it's the next thread id to be assigned), the
808            // new message will be merged with the draft message thread, causing confusion!
809            if (!TextUtils.isEmpty(content)) {
810                asyncUpdateDraftSmsMessage(mConversation, content);
811                mHasSmsDraft = true;
812            } else {
813                // When there's no associated text message, we have to handle the case where there
814                // might have been a previous mms draft for this message. This can happen when a
815                // user turns an mms back into a sms, such as creating an mms draft with a picture,
816                // then removing the picture.
817                asyncDeleteDraftMmsMessage(mConversation);
818            }
819        }
820
821        // Update state of the draft cache.
822        mConversation.setDraftState(true);
823    }
824
825    synchronized public void discard() {
826        if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
827            LogTag.debug("[WorkingMessage] discard");
828        }
829
830        if (mDiscarded == true) {
831            return;
832        }
833
834        // Mark this message as discarded in order to make saveDraft() no-op.
835        mDiscarded = true;
836
837        // Delete any associated drafts if there are any.
838        if (mHasMmsDraft) {
839            asyncDeleteDraftMmsMessage(mConversation);
840        }
841        if (mHasSmsDraft) {
842            asyncDeleteDraftSmsMessage(mConversation);
843        }
844        clearConversation(mConversation, true);
845    }
846
847    public void unDiscard() {
848        if (DEBUG) LogTag.debug("unDiscard");
849
850        mDiscarded = false;
851    }
852
853    /**
854     * Returns true if discard() has been called on this message.
855     */
856    public boolean isDiscarded() {
857        return mDiscarded;
858    }
859
860    /**
861     * Update the temporary list of recipients, used when setting up a
862     * new conversation.  Will be converted to a ContactList on any
863     * save event (send, save draft, etc.)
864     */
865    public void setWorkingRecipients(List<String> numbers) {
866        mWorkingRecipients = numbers;
867        String s = null;
868        if (numbers != null) {
869            int size = numbers.size();
870            switch (size) {
871            case 1:
872                s = numbers.get(0);
873                break;
874            case 0:
875                s = "empty";
876                break;
877            default:
878                s = "{...} len=" + size;
879            }
880        }
881        Log.i(TAG, "setWorkingRecipients: numbers=" + s);
882    }
883
884    private void dumpWorkingRecipients() {
885        Log.i(TAG, "-- mWorkingRecipients:");
886
887        if (mWorkingRecipients != null) {
888            int count = mWorkingRecipients.size();
889            for (int i=0; i<count; i++) {
890                Log.i(TAG, "   [" + i + "] " + mWorkingRecipients.get(i));
891            }
892            Log.i(TAG, "");
893        }
894    }
895
896    public void dump() {
897        Log.i(TAG, "WorkingMessage:");
898        dumpWorkingRecipients();
899        if (mConversation != null) {
900            Log.i(TAG, "mConversation: " + mConversation.toString());
901        }
902    }
903
904    /**
905     * Set the conversation associated with this message.
906     */
907    public void setConversation(Conversation conv) {
908        if (DEBUG) LogTag.debug("setConversation %s -> %s", mConversation, conv);
909
910        mConversation = conv;
911
912        // Convert to MMS if there are any email addresses in the recipient list.
913        setHasEmail(conv.getRecipients().containsEmail(), false);
914    }
915
916    public Conversation getConversation() {
917        return mConversation;
918    }
919
920    /**
921     * Hint whether or not this message will be delivered to an
922     * an email address.
923     */
924    public void setHasEmail(boolean hasEmail, boolean notify) {
925        if (MmsConfig.getEmailGateway() != null) {
926            updateState(RECIPIENTS_REQUIRE_MMS, false, notify);
927        } else {
928            updateState(RECIPIENTS_REQUIRE_MMS, hasEmail, notify);
929        }
930    }
931
932    /**
933     * Returns true if this message would require MMS to send.
934     */
935    public boolean requiresMms() {
936        return (mMmsState > 0);
937    }
938
939    /**
940     * Set whether or not we want to send this message via MMS in order to
941     * avoid sending an excessive number of concatenated SMS messages.
942     * @param: mmsRequired is the value for the LENGTH_REQUIRES_MMS bit.
943     * @param: notify Whether or not to notify the user.
944    */
945    public void setLengthRequiresMms(boolean mmsRequired, boolean notify) {
946        updateState(LENGTH_REQUIRES_MMS, mmsRequired, notify);
947    }
948
949    private static String stateString(int state) {
950        if (state == 0)
951            return "<none>";
952
953        StringBuilder sb = new StringBuilder();
954        if ((state & RECIPIENTS_REQUIRE_MMS) > 0)
955            sb.append("RECIPIENTS_REQUIRE_MMS | ");
956        if ((state & HAS_SUBJECT) > 0)
957            sb.append("HAS_SUBJECT | ");
958        if ((state & HAS_ATTACHMENT) > 0)
959            sb.append("HAS_ATTACHMENT | ");
960        if ((state & LENGTH_REQUIRES_MMS) > 0)
961            sb.append("LENGTH_REQUIRES_MMS | ");
962        if ((state & FORCE_MMS) > 0)
963            sb.append("FORCE_MMS | ");
964
965        sb.delete(sb.length() - 3, sb.length());
966        return sb.toString();
967    }
968
969    /**
970     * Sets the current state of our various "MMS required" bits.
971     *
972     * @param state The bit to change, such as {@link HAS_ATTACHMENT}
973     * @param on If true, set it; if false, clear it
974     * @param notify Whether or not to notify the user
975     */
976    private void updateState(int state, boolean on, boolean notify) {
977        if (!sMmsEnabled) {
978            // If Mms isn't enabled, the rest of the Messaging UI should not be using any
979            // feature that would cause us to to turn on any Mms flag and show the
980            // "Converting to multimedia..." message.
981            return;
982        }
983        int oldState = mMmsState;
984        if (on) {
985            mMmsState |= state;
986        } else {
987            mMmsState &= ~state;
988        }
989
990        // If we are clearing the last bit that is not FORCE_MMS,
991        // expire the FORCE_MMS bit.
992        if (mMmsState == FORCE_MMS && ((oldState & ~FORCE_MMS) > 0)) {
993            mMmsState = 0;
994        }
995
996        // Notify the listener if we are moving from SMS to MMS
997        // or vice versa.
998        if (notify) {
999            if (oldState == 0 && mMmsState != 0) {
1000                mStatusListener.onProtocolChanged(true);
1001            } else if (oldState != 0 && mMmsState == 0) {
1002                mStatusListener.onProtocolChanged(false);
1003            }
1004        }
1005
1006        if (oldState != mMmsState) {
1007            if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) LogTag.debug("updateState: %s%s = %s",
1008                    on ? "+" : "-",
1009                    stateString(state), stateString(mMmsState));
1010        }
1011    }
1012
1013    /**
1014     * Send this message over the network.  Will call back with onMessageSent() once
1015     * it has been dispatched to the telephony stack.  This WorkingMessage object is
1016     * no longer useful after this method has been called.
1017     *
1018     * @throws ContentRestrictionException if sending an MMS and uaProfUrl is not defined
1019     * in mms_config.xml.
1020     */
1021    public void send(final String recipientsInUI) {
1022        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
1023            LogTag.debug("send");
1024        }
1025        long origThreadId = mConversation.getThreadId();
1026
1027        removeSubjectIfEmpty(true /* notify */);
1028
1029        // Get ready to write to disk.
1030        prepareForSave(true /* notify */);
1031
1032        // We need the recipient list for both SMS and MMS.
1033        final Conversation conv = mConversation;
1034        String msgTxt = mText.toString();
1035
1036        if (requiresMms() || addressContainsEmailToMms(conv, msgTxt)) {
1037            // uaProfUrl setting in mms_config.xml must be present to send an MMS.
1038            // However, SMS service will still work in the absence of a uaProfUrl address.
1039            if (MmsConfig.getUaProfUrl() == null) {
1040                String err = "WorkingMessage.send MMS sending failure. mms_config.xml is " +
1041                        "missing uaProfUrl setting.  uaProfUrl is required for MMS service, " +
1042                        "but can be absent for SMS.";
1043                RuntimeException ex = new ContentRestrictionException(err);
1044                Log.e(TAG, err, ex);
1045                // now, let's just crash.
1046                throw ex;
1047            }
1048
1049            // Make local copies of the bits we need for sending a message,
1050            // because we will be doing it off of the main thread, which will
1051            // immediately continue on to resetting some of this state.
1052            final Uri mmsUri = mMessageUri;
1053            final PduPersister persister = PduPersister.getPduPersister(mActivity);
1054
1055            final SlideshowModel slideshow = mSlideshow;
1056            final CharSequence subject = mSubject;
1057
1058            // Do the dirty work of sending the message off of the main UI thread.
1059            new Thread(new Runnable() {
1060                public void run() {
1061                    final SendReq sendReq = makeSendReq(conv, subject);
1062
1063                    // Make sure the text in slide 0 is no longer holding onto a reference to
1064                    // the text in the message text box.
1065                    slideshow.prepareForSend();
1066                    sendMmsWorker(conv, mmsUri, persister, slideshow, sendReq);
1067
1068                    updateSendStats(conv);
1069                }
1070            }).start();
1071        } else {
1072            // Same rules apply as above.
1073            final String msgText = mText.toString();
1074            new Thread(new Runnable() {
1075                public void run() {
1076                    preSendSmsWorker(conv, msgText, recipientsInUI);
1077
1078                    updateSendStats(conv);
1079                }
1080            }).start();
1081        }
1082
1083        // update the Recipient cache with the new to address, if it's different
1084        RecipientIdCache.updateNumbers(conv.getThreadId(), conv.getRecipients());
1085
1086        // Mark the message as discarded because it is "off the market" after being sent.
1087        mDiscarded = true;
1088    }
1089
1090    // Be sure to only call this on a background thread.
1091    private void updateSendStats(final Conversation conv) {
1092        String[] dests = conv.getRecipients().getNumbers();
1093        final ArrayList<String> phoneNumbers = new ArrayList<String>(Arrays.asList(dests));
1094
1095        DataUsageStatUpdater updater = new DataUsageStatUpdater(mActivity);
1096        updater.updateWithPhoneNumber(phoneNumbers);
1097    }
1098
1099    private boolean addressContainsEmailToMms(Conversation conv, String text) {
1100        if (MmsConfig.getEmailGateway() != null) {
1101            String[] dests = conv.getRecipients().getNumbers();
1102            int length = dests.length;
1103            for (int i = 0; i < length; i++) {
1104                if (Mms.isEmailAddress(dests[i]) || MessageUtils.isAlias(dests[i])) {
1105                    String mtext = dests[i] + " " + text;
1106                    int[] params = SmsMessage.calculateLength(mtext, false);
1107                    if (params[0] > 1) {
1108                        updateState(RECIPIENTS_REQUIRE_MMS, true, true);
1109                        ensureSlideshow();
1110                        syncTextToSlideshow();
1111                        return true;
1112                    }
1113                }
1114            }
1115        }
1116        return false;
1117    }
1118
1119    // Message sending stuff
1120
1121    private void preSendSmsWorker(Conversation conv, String msgText, String recipientsInUI) {
1122        // If user tries to send the message, it's a signal the inputted text is what they wanted.
1123        UserHappinessSignals.userAcceptedImeText(mActivity);
1124
1125        mStatusListener.onPreMessageSent();
1126
1127        long origThreadId = conv.getThreadId();
1128
1129        // Make sure we are still using the correct thread ID for our recipient set.
1130        long threadId = conv.ensureThreadId();
1131
1132        String semiSepRecipients = conv.getRecipients().serialize();
1133
1134        // recipientsInUI can be empty when the user types in a number and hits send
1135        if (LogTag.SEVERE_WARNING && ((origThreadId != 0 && origThreadId != threadId) ||
1136               (!semiSepRecipients.equals(recipientsInUI) && !TextUtils.isEmpty(recipientsInUI)))) {
1137            String msg = origThreadId != 0 && origThreadId != threadId ?
1138                    "WorkingMessage.preSendSmsWorker threadId changed or " +
1139                    "recipients changed. origThreadId: " +
1140                    origThreadId + " new threadId: " + threadId +
1141                    " also mConversation.getThreadId(): " +
1142                    mConversation.getThreadId()
1143                :
1144                    "Recipients in window: \"" +
1145                    recipientsInUI + "\" differ from recipients from conv: \"" +
1146                    semiSepRecipients + "\"";
1147
1148            LogTag.warnPossibleRecipientMismatch(msg, mActivity);
1149        }
1150
1151        // just do a regular send. We're already on a non-ui thread so no need to fire
1152        // off another thread to do this work.
1153        sendSmsWorker(msgText, semiSepRecipients, threadId);
1154
1155        // Be paranoid and clean any draft SMS up.
1156        deleteDraftSmsMessage(threadId);
1157    }
1158
1159    private void sendSmsWorker(String msgText, String semiSepRecipients, long threadId) {
1160        String[] dests = TextUtils.split(semiSepRecipients, ";");
1161        if (LogTag.VERBOSE || Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
1162            Log.d(LogTag.TRANSACTION, "sendSmsWorker sending message: recipients=" +
1163                    semiSepRecipients + ", threadId=" + threadId);
1164        }
1165        MessageSender sender = new SmsMessageSender(mActivity, dests, msgText, threadId);
1166        try {
1167            sender.sendMessage(threadId);
1168
1169            // Make sure this thread isn't over the limits in message count
1170            Recycler.getSmsRecycler().deleteOldMessagesByThreadId(mActivity, threadId);
1171        } catch (Exception e) {
1172            Log.e(TAG, "Failed to send SMS message, threadId=" + threadId, e);
1173        }
1174
1175        mStatusListener.onMessageSent();
1176    }
1177
1178    private void sendMmsWorker(Conversation conv, Uri mmsUri, PduPersister persister,
1179                               SlideshowModel slideshow, SendReq sendReq) {
1180        // If user tries to send the message, it's a signal the inputted text is what they wanted.
1181        UserHappinessSignals.userAcceptedImeText(mActivity);
1182
1183        // First make sure we don't have too many outstanding unsent message.
1184        Cursor cursor = null;
1185        try {
1186            cursor = SqliteWrapper.query(mActivity, mContentResolver,
1187                    Mms.Outbox.CONTENT_URI, MMS_OUTBOX_PROJECTION, null, null, null);
1188            if (cursor != null) {
1189                long maxMessageSize = MmsConfig.getMaxSizeScaleForPendingMmsAllowed() *
1190                    MmsConfig.getMaxMessageSize();
1191                long totalPendingSize = 0;
1192                while (cursor.moveToNext()) {
1193                    totalPendingSize += cursor.getLong(MMS_MESSAGE_SIZE_INDEX);
1194                }
1195                if (totalPendingSize >= maxMessageSize) {
1196                    unDiscard();    // it wasn't successfully sent. Allow it to be saved as a draft.
1197                    mStatusListener.onMaxPendingMessagesReached();
1198                    return;
1199                }
1200            }
1201        } finally {
1202            if (cursor != null) {
1203                cursor.close();
1204            }
1205        }
1206        mStatusListener.onPreMessageSent();
1207        long threadId = 0;
1208
1209        try {
1210            DraftCache.getInstance().setSavingDraft(true);
1211
1212            // Make sure we are still using the correct thread ID for our
1213            // recipient set.
1214            threadId = conv.ensureThreadId();
1215
1216            if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1217                LogTag.debug("sendMmsWorker: update draft MMS message " + mmsUri);
1218            }
1219
1220            // One last check to verify the address of the recipient.
1221            String[] dests = conv.getRecipients().getNumbers(true /* scrub for MMS address */);
1222            if (dests.length == 1) {
1223                // verify the single address matches what's in the database. If we get a different
1224                // address back, jam the new value back into the SendReq.
1225                String newAddress =
1226                    Conversation.verifySingleRecipient(mActivity, conv.getThreadId(), dests[0]);
1227
1228                if (!newAddress.equals(dests[0])) {
1229                    dests[0] = newAddress;
1230                    EncodedStringValue[] encodedNumbers = EncodedStringValue.encodeStrings(dests);
1231                    if (encodedNumbers != null) {
1232                        sendReq.setTo(encodedNumbers);
1233                    }
1234                }
1235            }
1236
1237            if (mmsUri == null) {
1238                // Create a new MMS message if one hasn't been made yet.
1239                mmsUri = createDraftMmsMessage(persister, sendReq, slideshow);
1240            } else {
1241                // Otherwise, sync the MMS message in progress to disk.
1242                updateDraftMmsMessage(mmsUri, persister, slideshow, sendReq);
1243            }
1244            // Be paranoid and clean any draft SMS up.
1245            deleteDraftSmsMessage(threadId);
1246        } finally {
1247            DraftCache.getInstance().setSavingDraft(false);
1248        }
1249
1250        // Resize all the resizeable attachments (e.g. pictures) to fit
1251        // in the remaining space in the slideshow.
1252        int error = 0;
1253        try {
1254            slideshow.finalResize(mmsUri);
1255        } catch (ExceedMessageSizeException e1) {
1256            error = MESSAGE_SIZE_EXCEEDED;
1257        } catch (MmsException e1) {
1258            error = UNKNOWN_ERROR;
1259        }
1260        if (error != 0) {
1261            markMmsMessageWithError(mmsUri);
1262            mStatusListener.onAttachmentError(error);
1263            return;
1264        }
1265
1266        MessageSender sender = new MmsMessageSender(mActivity, mmsUri,
1267                slideshow.getCurrentMessageSize());
1268        try {
1269            if (!sender.sendMessage(threadId)) {
1270                // The message was sent through SMS protocol, we should
1271                // delete the copy which was previously saved in MMS drafts.
1272                SqliteWrapper.delete(mActivity, mContentResolver, mmsUri, null, null);
1273            }
1274
1275            // Make sure this thread isn't over the limits in message count
1276            Recycler.getMmsRecycler().deleteOldMessagesByThreadId(mActivity, threadId);
1277        } catch (Exception e) {
1278            Log.e(TAG, "Failed to send message: " + mmsUri + ", threadId=" + threadId, e);
1279        }
1280
1281        mStatusListener.onMessageSent();
1282    }
1283
1284    private void markMmsMessageWithError(Uri mmsUri) {
1285        try {
1286            PduPersister p = PduPersister.getPduPersister(mActivity);
1287            // Move the message into MMS Outbox. A trigger will create an entry in
1288            // the "pending_msgs" table.
1289            p.move(mmsUri, Mms.Outbox.CONTENT_URI);
1290
1291            // Now update the pending_msgs table with an error for that new item.
1292            ContentValues values = new ContentValues(1);
1293            values.put(PendingMessages.ERROR_TYPE, MmsSms.ERR_TYPE_GENERIC_PERMANENT);
1294            long msgId = ContentUris.parseId(mmsUri);
1295            SqliteWrapper.update(mActivity, mContentResolver,
1296                    PendingMessages.CONTENT_URI,
1297                    values, PendingMessages.MSG_ID + "=" + msgId, null);
1298        } catch (MmsException e) {
1299            // Not much we can do here. If the p.move throws an exception, we'll just
1300            // leave the message in the draft box.
1301            Log.e(TAG, "Failed to move message to outbox and mark as error: " + mmsUri, e);
1302        }
1303    }
1304
1305    // Draft message stuff
1306
1307    private static final String[] MMS_DRAFT_PROJECTION = {
1308        Mms._ID,                // 0
1309        Mms.SUBJECT,            // 1
1310        Mms.SUBJECT_CHARSET     // 2
1311    };
1312
1313    private static final int MMS_ID_INDEX         = 0;
1314    private static final int MMS_SUBJECT_INDEX    = 1;
1315    private static final int MMS_SUBJECT_CS_INDEX = 2;
1316
1317    private static Uri readDraftMmsMessage(Context context, Conversation conv, StringBuilder sb) {
1318        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1319            LogTag.debug("readDraftMmsMessage conv: " + conv);
1320        }
1321        Cursor cursor;
1322        ContentResolver cr = context.getContentResolver();
1323
1324        final String selection = Mms.THREAD_ID + " = " + conv.getThreadId();
1325        cursor = SqliteWrapper.query(context, cr,
1326                Mms.Draft.CONTENT_URI, MMS_DRAFT_PROJECTION,
1327                selection, null, null);
1328
1329        Uri uri;
1330        try {
1331            if (cursor.moveToFirst()) {
1332                uri = ContentUris.withAppendedId(Mms.Draft.CONTENT_URI,
1333                        cursor.getLong(MMS_ID_INDEX));
1334                String subject = MessageUtils.extractEncStrFromCursor(cursor, MMS_SUBJECT_INDEX,
1335                        MMS_SUBJECT_CS_INDEX);
1336                if (subject != null) {
1337                    sb.append(subject);
1338                }
1339                if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1340                    LogTag.debug("readDraftMmsMessage uri: ", uri);
1341                }
1342                return uri;
1343            }
1344        } finally {
1345            cursor.close();
1346        }
1347
1348        return null;
1349    }
1350
1351    /**
1352     * makeSendReq should always return a non-null SendReq, whether the dest addresses are
1353     * valid or not.
1354     */
1355    private static SendReq makeSendReq(Conversation conv, CharSequence subject) {
1356        String[] dests = conv.getRecipients().getNumbers(true /* scrub for MMS address */);
1357
1358        SendReq req = new SendReq();
1359        EncodedStringValue[] encodedNumbers = EncodedStringValue.encodeStrings(dests);
1360        if (encodedNumbers != null) {
1361            req.setTo(encodedNumbers);
1362        }
1363
1364        if (!TextUtils.isEmpty(subject)) {
1365            req.setSubject(new EncodedStringValue(subject.toString()));
1366        }
1367
1368        req.setDate(System.currentTimeMillis() / 1000L);
1369
1370        return req;
1371    }
1372
1373    private static Uri createDraftMmsMessage(PduPersister persister, SendReq sendReq,
1374            SlideshowModel slideshow) {
1375        try {
1376            PduBody pb = slideshow.toPduBody();
1377            sendReq.setBody(pb);
1378            Uri res = persister.persist(sendReq, Mms.Draft.CONTENT_URI);
1379            slideshow.sync(pb);
1380            return res;
1381        } catch (MmsException e) {
1382            return null;
1383        }
1384    }
1385
1386    private void asyncUpdateDraftMmsMessage(final Conversation conv, final boolean isStopping) {
1387        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1388            LogTag.debug("asyncUpdateDraftMmsMessage conv=%s mMessageUri=%s", conv, mMessageUri);
1389        }
1390
1391        new Thread(new Runnable() {
1392            public void run() {
1393                try {
1394                    DraftCache.getInstance().setSavingDraft(true);
1395                    final PduPersister persister = PduPersister.getPduPersister(mActivity);
1396                    final SendReq sendReq = makeSendReq(conv, mSubject);
1397
1398                    if (mMessageUri == null) {
1399                        mMessageUri = createDraftMmsMessage(persister, sendReq, mSlideshow);
1400                    } else {
1401                        updateDraftMmsMessage(mMessageUri, persister, mSlideshow, sendReq);
1402                    }
1403                    if (isStopping && conv.getMessageCount() == 0) {
1404                        // createDraftMmsMessage can create the new thread in the threads table (the
1405                        // call to createDraftMmsDraftMessage calls PduPersister.persist() which
1406                        // can call Threads.getOrCreateThreadId()). Meanwhile, when the user goes
1407                        // back to ConversationList while we're saving a draft from CMA's.onStop,
1408                        // ConversationList will delete all threads from the thread table that
1409                        // don't have associated sms or pdu entries. In case our thread got deleted,
1410                        // well call clearThreadId() so ensureThreadId will query the db for the new
1411                        // thread.
1412                        conv.clearThreadId();   // force us to get the updated thread id
1413                    }
1414                    conv.ensureThreadId();
1415                    conv.setDraftState(true);
1416                    if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1417                        LogTag.debug("asyncUpdateDraftMmsMessage conv: " + conv +
1418                                " uri: " + mMessageUri);
1419                    }
1420
1421                    // Be paranoid and delete any SMS drafts that might be lying around. Must do
1422                    // this after ensureThreadId so conv has the correct thread id.
1423                    asyncDeleteDraftSmsMessage(conv);
1424                } finally {
1425                    DraftCache.getInstance().setSavingDraft(false);
1426                }
1427            }
1428        }).start();
1429    }
1430
1431    private static void updateDraftMmsMessage(Uri uri, PduPersister persister,
1432            SlideshowModel slideshow, SendReq sendReq) {
1433        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1434            LogTag.debug("updateDraftMmsMessage uri=%s", uri);
1435        }
1436        if (uri == null) {
1437            Log.e(TAG, "updateDraftMmsMessage null uri");
1438            return;
1439        }
1440        persister.updateHeaders(uri, sendReq);
1441        final PduBody pb = slideshow.toPduBody();
1442
1443        try {
1444            persister.updateParts(uri, pb);
1445        } catch (MmsException e) {
1446            Log.e(TAG, "updateDraftMmsMessage: cannot update message " + uri);
1447        }
1448
1449        slideshow.sync(pb);
1450    }
1451
1452    private static final String SMS_DRAFT_WHERE = Sms.TYPE + "=" + Sms.MESSAGE_TYPE_DRAFT;
1453    private static final String[] SMS_BODY_PROJECTION = { Sms.BODY };
1454    private static final int SMS_BODY_INDEX = 0;
1455
1456    /**
1457     * Reads a draft message for the given thread ID from the database,
1458     * if there is one, deletes it from the database, and returns it.
1459     * @return The draft message or an empty string.
1460     */
1461    private String readDraftSmsMessage(Conversation conv) {
1462        long thread_id = conv.getThreadId();
1463        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1464            Log.d(TAG, "readDraftSmsMessage conv: " + conv);
1465        }
1466        // If it's an invalid thread or we know there's no draft, don't bother.
1467        if (thread_id <= 0 || !conv.hasDraft()) {
1468            return "";
1469        }
1470
1471        Uri thread_uri = ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, thread_id);
1472        String body = "";
1473
1474        Cursor c = SqliteWrapper.query(mActivity, mContentResolver,
1475                        thread_uri, SMS_BODY_PROJECTION, SMS_DRAFT_WHERE, null, null);
1476        boolean haveDraft = false;
1477        if (c != null) {
1478            try {
1479                if (c.moveToFirst()) {
1480                    body = c.getString(SMS_BODY_INDEX);
1481                    haveDraft = true;
1482                }
1483            } finally {
1484                c.close();
1485            }
1486        }
1487
1488        // We found a draft, and if there are no messages in the conversation,
1489        // that means we deleted the thread, too. Must reset the thread id
1490        // so we'll eventually create a new thread.
1491        if (haveDraft && conv.getMessageCount() == 0) {
1492            asyncDeleteDraftSmsMessage(conv);
1493
1494            // Clean out drafts for this thread -- if the recipient set changes,
1495            // we will lose track of the original draft and be unable to delete
1496            // it later.  The message will be re-saved if necessary upon exit of
1497            // the activity.
1498            clearConversation(conv, true);
1499        }
1500        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1501            LogTag.debug("readDraftSmsMessage haveDraft: ", !TextUtils.isEmpty(body));
1502        }
1503
1504        return body;
1505    }
1506
1507    public void clearConversation(final Conversation conv, boolean resetThreadId) {
1508        if (resetThreadId && conv.getMessageCount() == 0) {
1509            if (DEBUG) LogTag.debug("clearConversation calling clearThreadId");
1510            conv.clearThreadId();
1511        }
1512
1513        conv.setDraftState(false);
1514    }
1515
1516    private void asyncUpdateDraftSmsMessage(final Conversation conv, final String contents) {
1517        new Thread(new Runnable() {
1518            public void run() {
1519                try {
1520                    DraftCache.getInstance().setSavingDraft(true);
1521                    long threadId = conv.ensureThreadId();
1522                    conv.setDraftState(true);
1523                    updateDraftSmsMessage(conv, contents);
1524                } finally {
1525                    DraftCache.getInstance().setSavingDraft(false);
1526                }
1527            }
1528        }).start();
1529    }
1530
1531    private void updateDraftSmsMessage(final Conversation conv, String contents) {
1532        final long threadId = conv.getThreadId();
1533        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1534            LogTag.debug("updateDraftSmsMessage tid=%d, contents=\"%s\"", threadId, contents);
1535        }
1536
1537        // If we don't have a valid thread, there's nothing to do.
1538        if (threadId <= 0) {
1539            return;
1540        }
1541
1542        ContentValues values = new ContentValues(3);
1543        values.put(Sms.THREAD_ID, threadId);
1544        values.put(Sms.BODY, contents);
1545        values.put(Sms.TYPE, Sms.MESSAGE_TYPE_DRAFT);
1546        SqliteWrapper.insert(mActivity, mContentResolver, Sms.CONTENT_URI, values);
1547        asyncDeleteDraftMmsMessage(conv);
1548    }
1549
1550    private void asyncDelete(final Uri uri, final String selection, final String[] selectionArgs) {
1551        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1552            LogTag.debug("asyncDelete %s where %s", uri, selection);
1553        }
1554        new Thread(new Runnable() {
1555            public void run() {
1556                SqliteWrapper.delete(mActivity, mContentResolver, uri, selection, selectionArgs);
1557            }
1558        }).start();
1559    }
1560
1561    public void asyncDeleteDraftSmsMessage(Conversation conv) {
1562        mHasSmsDraft = false;
1563
1564        final long threadId = conv.getThreadId();
1565        if (threadId > 0) {
1566            asyncDelete(ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, threadId),
1567                SMS_DRAFT_WHERE, null);
1568        }
1569    }
1570
1571    private void deleteDraftSmsMessage(long threadId) {
1572        SqliteWrapper.delete(mActivity, mContentResolver,
1573                ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, threadId),
1574                SMS_DRAFT_WHERE, null);
1575    }
1576
1577    private void asyncDeleteDraftMmsMessage(Conversation conv) {
1578        mHasMmsDraft = false;
1579
1580        final long threadId = conv.getThreadId();
1581        if (threadId > 0) {
1582            final String where = Mms.THREAD_ID + " = " + threadId;
1583            asyncDelete(Mms.Draft.CONTENT_URI, where, null);
1584        }
1585    }
1586}
1587