WorkingMessage.java revision b9bcfdd226bbb6f5b265f925343375192963d58a
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 com.android.mms.ExceedMessageSizeException;
20import com.android.mms.ResolutionException;
21import com.android.mms.UnsupportContentTypeException;
22import com.android.mms.model.AudioModel;
23import com.android.mms.model.ImageModel;
24import com.android.mms.model.MediaModel;
25import com.android.mms.model.SlideModel;
26import com.android.mms.model.SlideshowModel;
27import com.android.mms.model.TextModel;
28import com.android.mms.model.VideoModel;
29import com.android.mms.transaction.MessageSender;
30import com.android.mms.transaction.MmsMessageSender;
31import com.android.mms.transaction.SmsMessageSender;
32import com.android.mms.ui.ComposeMessageActivity;
33import com.android.mms.ui.RecipientList;
34import com.android.mms.util.DraftCache;
35import com.google.android.mms.ContentType;
36import com.google.android.mms.MmsException;
37import com.google.android.mms.pdu.EncodedStringValue;
38import com.google.android.mms.pdu.PduBody;
39import com.google.android.mms.pdu.PduPersister;
40import com.google.android.mms.pdu.SendReq;
41import com.google.android.mms.util.SqliteWrapper;
42
43import java.util.Arrays;
44import java.util.HashSet;
45
46import android.content.ContentResolver;
47import android.content.ContentUris;
48import android.content.ContentValues;
49import android.content.Context;
50import android.database.Cursor;
51import android.net.Uri;
52import android.os.Bundle;
53import android.provider.Telephony.Mms;
54import android.provider.Telephony.Sms;
55import android.provider.Telephony.Threads;
56import android.text.TextUtils;
57import android.util.Log;
58
59/**
60 * Contains all state related to a message being edited by the user.
61 */
62public class WorkingMessage {
63    private static final String TAG = "WorkingMessage";
64    private static final boolean DEBUG = false;
65
66    // Database access stuff
67    private final Context mContext;
68    private final ContentResolver mContentResolver;
69
70    // States that can require us to save or send a message as MMS.
71    private static final int RECIPIENTS_REQUIRE_MMS = (1 << 0);     // 1
72    private static final int HAS_SUBJECT = (1 << 1);                // 2
73    private static final int HAS_ATTACHMENT = (1 << 2);             // 4
74    private static final int LENGTH_REQUIRES_MMS = (1 << 3);        // 8
75    private static final int FORCE_MMS = (1 << 4);                  // 16
76
77    // A bitmap of the above indicating different properties of the message;
78    // any bit set will require the message to be sent via MMS.
79    private int mMmsState;
80
81    // Errors from setAttachment()
82    public static final int OK = 0;
83    public static final int UNKNOWN_ERROR = -1;
84    public static final int MESSAGE_SIZE_EXCEEDED = -2;
85    public static final int UNSUPPORTED_TYPE = -3;
86    public static final int IMAGE_TOO_LARGE = -4;
87
88    // Attachment types
89    public static final int TEXT = 0;
90    public static final int IMAGE = 1;
91    public static final int VIDEO = 2;
92    public static final int AUDIO = 3;
93    public static final int SLIDESHOW = 4;
94
95    // Current attachment type of the message; one of the above values.
96    private int mAttachmentType;
97
98    // Current recipient set for this message.
99    private String[] mRecipients;
100    // ID in the threads table for the conversation this message is in.
101    private long mThreadId;
102
103    // Text of the message.
104    private CharSequence mText;
105    // Slideshow for this message, if applicable.  If it's a simple attachment,
106    // i.e. not SLIDESHOW, it will contain only one slide.
107    private SlideshowModel mSlideshow;
108    // Data URI of an MMS message if we have had to save it.
109    private Uri mMessageUri;
110    // MMS subject line for this message
111    private CharSequence mSubject;
112
113    // Set to true if this message has been discarded.
114    private boolean mDiscarded = false;
115
116    // Our callback interface
117    private final MessageStatusListener mStatusListener;
118
119    /**
120     * Callback interface for communicating important state changes back to
121     * ComposeMessageActivity.
122     */
123    public interface MessageStatusListener {
124        /**
125         * Called when the protocol for sending the message changes from SMS
126         * to MMS, and vice versa.
127         *
128         * @param mms If true, it changed to MMS.  If false, to SMS.
129         */
130        void onProtocolChanged(boolean mms);
131
132        /**
133         * Called when an attachment on the message has changed.
134         */
135        void onAttachmentChanged();
136
137        /**
138         * Called once the process of sending a message, triggered by
139         * {@link send} has completed. This doesn't mean the send succeeded,
140         * just that it has been dispatched to the network.
141         *
142         * @param threadId Thread ID the message was stored to
143         */
144        void onMessageSent(long threadId);
145    }
146
147    private WorkingMessage(ComposeMessageActivity activity) {
148        mContext = activity;
149        mContentResolver = mContext.getContentResolver();
150        mStatusListener = activity;
151        mAttachmentType = TEXT;
152        mText = "";
153    }
154
155    /**
156     * Creates a new working message.
157     */
158    public static WorkingMessage createEmpty(ComposeMessageActivity activity) {
159        // Make a new empty working message.
160        WorkingMessage msg = new WorkingMessage(activity);
161        return msg;
162    }
163
164    /**
165     * Create a new WorkingMessage from the specified data URI, which typically
166     * contains an MMS message.
167     */
168    public static WorkingMessage load(ComposeMessageActivity activity, Uri uri) {
169        // If the message is not already in the draft box, move it there.
170        if (!uri.toString().startsWith(Mms.Draft.CONTENT_URI.toString())) {
171            PduPersister persister = PduPersister.getPduPersister(activity);
172            if (DEBUG) debug("load: moving %s to drafts", uri);
173            try {
174                uri = persister.move(uri, Mms.Draft.CONTENT_URI);
175            } catch (MmsException e) {
176                error("Can't move %s to drafts", uri);
177                return null;
178            }
179        }
180
181        WorkingMessage msg = new WorkingMessage(activity);
182        if (msg.loadFromUri(uri)) {
183            return msg;
184        }
185
186        return null;
187    }
188
189    private void correctAttachmentState() {
190        if (mSlideshow == null || mSlideshow.size() == 0) {
191            throw new IllegalStateException("corrrectState() called with no slideshow");
192        }
193
194        if (mSlideshow.size() > 1) {
195            mAttachmentType = SLIDESHOW;
196        } else {
197            SlideModel slide = mSlideshow.get(0);
198            if (slide.hasImage()) {
199                mAttachmentType = IMAGE;
200            } else if (slide.hasVideo()) {
201                mAttachmentType = VIDEO;
202            } else if (slide.hasAudio()) {
203                mAttachmentType = AUDIO;
204            }
205        }
206
207        updateState(HAS_ATTACHMENT, hasAttachment(), false);
208    }
209
210    private boolean loadFromUri(Uri uri) {
211        if (DEBUG) debug("loadFromUri %s", uri);
212        try {
213            mSlideshow = SlideshowModel.createFromMessageUri(mContext, uri);
214        } catch (MmsException e) {
215            error("Couldn't load URI %s", uri);
216            return false;
217        }
218
219        mMessageUri = uri;
220
221        // Make sure all our state is as expected.
222        syncTextFromSlideshow();
223        correctAttachmentState();
224
225        return true;
226    }
227
228    /**
229     * Load the draft message for the specified ID, or a new empty message if
230     * none exists.
231     */
232    public static WorkingMessage loadDraft(ComposeMessageActivity activity, long threadId) {
233        WorkingMessage msg = new WorkingMessage(activity);
234        if (msg.loadFromConversation(threadId)) {
235            return msg;
236        } else {
237            return createEmpty(activity);
238        }
239    }
240
241    private boolean loadFromConversation(long threadId) {
242        if (DEBUG) debug("loadFromConversation %d", threadId);
243
244        // Look for an SMS draft first.
245        mText = readDraftSmsMessage(mContext, threadId);
246        if (!TextUtils.isEmpty(mText)) {
247            return true;
248        }
249
250        // Then look for an MMS draft.
251        StringBuilder sb = new StringBuilder();
252        Uri uri = readDraftMmsMessage(mContext, threadId, sb);
253        if (uri != null) {
254            if (loadFromUri(uri)) {
255                // If there was an MMS message, readDraftMmsMessage
256                // will put the subject in our supplied StringBuilder.
257                if (sb.length() > 0) {
258                    setSubject(sb.toString());
259                }
260                return true;
261            }
262        }
263
264        return false;
265    }
266
267    /**
268     * Sets the text of the message to the specified CharSequence.
269     */
270    public void setText(CharSequence s) {
271        mText = s;
272    }
273
274    /**
275     * Returns the current message text.
276     */
277    public CharSequence getText() {
278        return mText;
279    }
280
281    /**
282     * Returns true if the message has any text.
283     * @return
284     */
285    public boolean hasText() {
286        return !TextUtils.isEmpty(mText);
287    }
288
289    /**
290     * Adds an attachment to the message, replacing an old one if it existed.
291     * @param type Type of this attachment, such as {@link IMAGE}
292     * @param dataUri Uri containing the attachment data (or null for {@link TEXT})
293     * @return An error code such as {@link UNKNOWN_ERROR} or {@link OK} if successful
294     */
295    public int setAttachment(int type, Uri dataUri) {
296        if (DEBUG) debug("setAttachment type=%d uri %s", type, dataUri);
297        int result = OK;
298
299        // Make sure mSlideshow is set up and has a slide.
300        ensureSlideshow();
301
302        // Change the attachment and translate the various underlying
303        // exceptions into useful error codes.
304        try {
305            changeMedia(type, dataUri);
306        } catch (MmsException e) {
307            result = UNKNOWN_ERROR;
308        } catch (UnsupportContentTypeException e) {
309            result = UNSUPPORTED_TYPE;
310        } catch (ExceedMessageSizeException e) {
311            result = MESSAGE_SIZE_EXCEEDED;
312        } catch (ResolutionException e) {
313            result = IMAGE_TOO_LARGE;
314        }
315
316        // If we were successful, update mAttachmentType and notify
317        // the listener than there was a change.
318        if (result == OK) {
319            mAttachmentType = type;
320            mStatusListener.onAttachmentChanged();
321        }
322
323        // Set HAS_ATTACHMENT if we need it.
324        updateState(HAS_ATTACHMENT, hasAttachment(), true);
325
326        return result;
327    }
328
329    /**
330     * Returns true if this message contains anything worth saving.
331     */
332    public boolean isWorthSaving() {
333        // If it actually contains anything, it's of course not empty.
334        if (hasText() || hasSubject() || hasAttachment() || hasSlideshow()) {
335            return true;
336        }
337
338        // When saveAsMms() has been called, we set FORCE_MMS to represent
339        // sort of an "invisible attachment" so that the message isn't thrown
340        // away when we are shipping it off to other activities.
341        if ((mMmsState & FORCE_MMS) > 0) {
342            return true;
343        }
344
345        return false;
346    }
347
348    /**
349     * Makes sure mSlideshow is set up.
350     */
351    private void ensureSlideshow() {
352        if (mSlideshow != null) {
353            return;
354        }
355
356        SlideshowModel slideshow = SlideshowModel.createNew(mContext);
357        SlideModel slide = new SlideModel(slideshow);
358        slideshow.add(slide);
359
360        mSlideshow = slideshow;
361    }
362
363    /**
364     * Change the message's attachment to the data in the specified Uri.
365     * Used only for single-slide ("attachment mode") messages.
366     */
367    private void changeMedia(int type, Uri uri) throws MmsException {
368        SlideModel slide = mSlideshow.get(0);
369        MediaModel media;
370
371        // Remove any previous attachments.
372        slide.removeImage();
373        slide.removeVideo();
374        slide.removeAudio();
375
376        // If we're changing to text, just bail out.
377        if (type == TEXT) {
378            return;
379        }
380
381        // Make a correct MediaModel for the type of attachment.
382        if (type == IMAGE) {
383            media = new ImageModel(mContext, uri, mSlideshow.getLayout().getImageRegion());
384        } else if (type == VIDEO) {
385            media = new VideoModel(mContext, uri, mSlideshow.getLayout().getImageRegion());
386        } else if (type == AUDIO) {
387            media = new AudioModel(mContext, uri);
388        } else {
389            throw new IllegalArgumentException("changeMedia type=" + type + ", uri=" + uri);
390        }
391
392        // Add it to the slide.
393        slide.add(media);
394
395        // For video and audio, set the duration of the slide to
396        // that of the attachment.
397        if (type == VIDEO || type == AUDIO) {
398            slide.updateDuration(media.getDuration());
399        }
400    }
401
402    /**
403     * Returns true if the message has an attachment (including slideshows).
404     */
405    public boolean hasAttachment() {
406        return (mAttachmentType > TEXT);
407    }
408
409    /**
410     * Returns the slideshow associated with this message.
411     */
412    public SlideshowModel getSlideshow() {
413        return mSlideshow;
414    }
415
416    /**
417     * Returns true if the message has a real slideshow, as opposed to just
418     * one image attachment, for example.
419     */
420    public boolean hasSlideshow() {
421        return (mAttachmentType == SLIDESHOW);
422    }
423
424    /**
425     * Sets the MMS subject of the message.  Passing null indicates that there
426     * is no subject.  Passing "" will result in an empty subject being added
427     * to the message, possibly triggering a conversion to MMS.  This extra
428     * bit of state is needed to support ComposeMessageActivity converting to
429     * MMS when the user adds a subject.  An empty subject will be removed
430     * before saving to disk or sending, however.
431     */
432    public void setSubject(CharSequence s) {
433        mSubject = s;
434        updateState(HAS_SUBJECT, (s != null), true);
435    }
436
437    /**
438     * Returns the MMS subject of the message.
439     */
440    public CharSequence getSubject() {
441        return mSubject;
442    }
443
444    /**
445     * Returns true if this message has an MMS subject.
446     * @return
447     */
448    public boolean hasSubject() {
449        return !TextUtils.isEmpty(mSubject);
450    }
451
452    /**
453     * Moves the message text into the slideshow.  Should be called any time
454     * the message is about to be sent or written to disk.
455     */
456    private void syncTextToSlideshow() {
457        if (mSlideshow == null || mSlideshow.size() != 1)
458            return;
459
460        SlideModel slide = mSlideshow.get(0);
461        TextModel text;
462        if (!slide.hasText()) {
463            // Add a TextModel to slide 0 if one doesn't already exist
464            text = new TextModel(mContext, ContentType.TEXT_PLAIN, "text_0.txt",
465                                           mSlideshow.getLayout().getTextRegion());
466            slide.add(text);
467        } else {
468            // Otherwise just reuse the existing one.
469            text = slide.getText();
470        }
471        text.setText(mText);
472    }
473
474    /**
475     * Sets the message text out of the slideshow.  Should be called any time
476     * a slideshow is loaded from disk.
477     */
478    private void syncTextFromSlideshow() {
479        // Don't sync text for real slideshows.
480        if (mSlideshow.size() != 1) {
481            return;
482        }
483
484        SlideModel slide = mSlideshow.get(0);
485        if (!slide.hasText()) {
486            return;
487        }
488
489        mText = slide.getText().getText();
490    }
491
492    /**
493     * Removes the subject if it is empty, possibly converting back to SMS.
494     */
495    private void removeSubjectIfEmpty() {
496        if (!hasSubject()) {
497            setSubject(null);
498        }
499    }
500
501    /**
502     * Gets internal message state ready for storage.  Should be called any
503     * time the message is about to be sent or written to disk.
504     */
505    private void prepareForSave() {
506        if (requiresMms()) {
507            ensureSlideshow();
508            syncTextToSlideshow();
509            removeSubjectIfEmpty();
510        }
511    }
512
513    /**
514     * Force the message to be saved as MMS and return the Uri of the message.
515     * Typically used when handing a message off to another activity.
516     */
517    public Uri saveAsMms() {
518        if (DEBUG) debug("save mRecipients=%s", (Object)mRecipients);
519
520        if (mDiscarded) {
521            throw new IllegalStateException("save() called after discard()");
522        }
523
524        // FORCE_MMS behaves as sort of an "invisible attachment", making
525        // the message seem non-empty (and thus not discarded).  This bit
526        // is sticky until the last other MMS bit is removed, at which
527        // point the message will fall back to SMS.
528        updateState(FORCE_MMS, true, false);
529
530        // Collect our state to be written to disk.
531        prepareForSave();
532
533        PduPersister persister = PduPersister.getPduPersister(mContext);
534        SendReq sendReq = makeSendReq(mRecipients, mSubject);
535
536        // Make sure we are saving to the correct thread ID.
537        mThreadId = getOrCreateThreadId(mRecipients);
538        DraftCache.getInstance().setDraftState(mThreadId, true);
539
540        // If we don't already have a Uri lying around, make a new one.  If we do
541        // have one already, make sure it is synced to disk.
542        if (mMessageUri == null) {
543            mMessageUri = createDraftMmsMessage(persister, sendReq, mSlideshow);
544        } else {
545            updateDraftMmsMessage(mMessageUri, persister, mSlideshow, sendReq);
546        }
547
548        return mMessageUri;
549    }
550
551    /**
552     * Save this message as a draft in the conversation previously specified
553     * to {@link setConversation}.
554     */
555    public void saveDraft() {
556        if (DEBUG) debug("saveDraft");
557
558        // If we have discarded the message, just bail out.
559        if (mDiscarded) {
560            return;
561        }
562
563        // Make sure setConversation was called.
564        if (mRecipients == null) {
565            throw new IllegalStateException("saveDraft() called with no recipients");
566        }
567
568        // Get ready to write to disk.
569        prepareForSave();
570
571        if (requiresMms()) {
572            asyncUpdateDraftMmsMessage(mRecipients);
573        } else {
574            asyncUpdateDraftSmsMessage(mRecipients, mText.toString());
575        }
576
577        // Update state of the draft cache.
578        DraftCache.getInstance().setDraftState(mThreadId, true);
579    }
580
581    public void discard() {
582        if (DEBUG) debug("discard");
583
584        // Technically, we could probably just bail out here.  But discard() is
585        // really meant to be called if you never want to use the message again,
586        // so keep this assert in as a debugging aid.
587        if (mDiscarded == true) {
588            throw new IllegalStateException("discard() called twice");
589        }
590
591        // Mark this message as discarded in order to make saveDraft() no-op.
592        mDiscarded = true;
593
594        // Delete our MMS message, if there is one.
595        if (mMessageUri != null) {
596            asyncDelete(mMessageUri, null, null);
597        }
598
599        // Delete any draft messages associated with this conversation.
600        asyncDeleteDraftSmsMessage(mThreadId);
601
602        // Update state of the draft cache.
603        DraftCache.getInstance().setDraftState(mThreadId, false);
604    }
605
606    /**
607     * Returns true if discard() has been called on this message.
608     */
609    public boolean isDiscarded() {
610        return mDiscarded;
611    }
612
613    /**
614     * To be called from our Activity's onSaveInstanceState() to give us a chance
615     * to stow our state away for later retrieval.
616     *
617     * @param bundle The Bundle passed in to onSaveInstanceState
618     */
619    public void writeStateToBundle(Bundle bundle) {
620        if (hasSubject()) {
621            bundle.putString("subject", mSubject.toString());
622        }
623
624        if (mMessageUri != null) {
625            bundle.putParcelable("msg_uri", mMessageUri);
626        } else if (hasText()) {
627            bundle.putString("sms_body", mText.toString());
628        }
629    }
630
631    /**
632     * To be called from our Activity's onCreate() if the activity manager
633     * has given it a Bundle to reinflate
634     * @param bundle The Bundle passed in to onCreate
635     */
636    public void readStateFromBundle(Bundle bundle) {
637        if (bundle == null) {
638            return;
639        }
640
641        String subject = bundle.getString("subject");
642        setSubject(subject);
643
644        Uri uri = (Uri)bundle.getParcelable("msg_uri");
645        if (uri != null) {
646            loadFromUri(uri);
647            return;
648        } else {
649            String body = bundle.getString("sms_body");
650            mText = body;
651        }
652    }
653
654    /**
655     * Set the conversation associated with this message.
656     *
657     * @param threadId ID in the threads table for the conversation or 0 if unknown
658     * @param recipients List of recipients for the conversation
659     */
660    public void setConversation(long threadId, RecipientList recipients) {
661        if (DEBUG) debug("setConversation %d/%s -> %d/%s", mThreadId, mRecipients, threadId, recipients.getToNumbers());
662
663        // Just stash local copies of the data for now.  We will use them
664        // to hit the disk later when we actually need to.  This method can
665        // be called a lot if the user is thrashing around in the
666        // recipients editor.
667        mThreadId = threadId;
668        mRecipients = recipients.getToNumbers();
669
670        // Convert to MMS if there are any email addresses in the recipient list.
671        updateState(RECIPIENTS_REQUIRE_MMS, recipients.containsEmail(), true);
672    }
673
674
675    /**
676     * Returns true if this message would require MMS to send.
677     */
678    public boolean requiresMms() {
679        return (mMmsState > 0);
680    }
681
682    /**
683     * Set whether or not we want to send this message via MMS in order to
684     * avoid sending an excessive number of concatenated SMS messages.
685     */
686    public void setLengthRequiresMms(boolean mmsRequired) {
687        updateState(LENGTH_REQUIRES_MMS, mmsRequired, true);
688    }
689
690    private static String stateString(int state) {
691        if (state == 0)
692            return "<none>";
693
694        StringBuilder sb = new StringBuilder();
695        if ((state & RECIPIENTS_REQUIRE_MMS) > 0)
696            sb.append("RECIPIENTS_REQUIRE_MMS | ");
697        if ((state & HAS_SUBJECT) > 0)
698            sb.append("HAS_SUBJECT | ");
699        if ((state & HAS_ATTACHMENT) > 0)
700            sb.append("HAS_ATTACHMENT | ");
701        if ((state & LENGTH_REQUIRES_MMS) > 0)
702            sb.append("LENGTH_REQUIRES_MMS | ");
703        if ((state & FORCE_MMS) > 0)
704            sb.append("FORCE_MMS | ");
705
706        sb.delete(sb.length() - 3, sb.length());
707        return sb.toString();
708    }
709
710    /**
711     * Sets the current state of our various "MMS required" bits.
712     *
713     * @param state The bit to change, such as {@link HAS_ATTACHMENT}
714     * @param on If true, set it; if false, clear it
715     * @param notify TODO
716     */
717    private void updateState(int state, boolean on, boolean notify) {
718        int oldState = mMmsState;
719        if (on) {
720            mMmsState |= state;
721        } else {
722            mMmsState &= ~state;
723        }
724
725        // If we are clearing the last bit that is not FORCE_MMS,
726        // expire the FORCE_MMS bit.
727        if (mMmsState == FORCE_MMS && ((oldState & ~FORCE_MMS) > 0)) {
728            mMmsState = 0;
729        }
730
731        // Notify the listener if we are moving from SMS to MMS
732        // or vice versa.
733        if (notify) {
734            if (oldState == 0 && mMmsState != 0) {
735                mStatusListener.onProtocolChanged(true);
736            } else if (oldState != 0 && mMmsState == 0) {
737                mStatusListener.onProtocolChanged(false);
738            }
739        }
740
741        if (oldState != mMmsState) {
742            if (DEBUG) debug("updateState: %s%s = %s", on ? "+" : "-",
743                    stateString(state), stateString(mMmsState));
744        }
745    }
746
747    /**
748     * Send this message over the network.  Will call back with onMessageSent() once
749     * it has been dispatched to the telephony stack.  This WorkingMessage object is
750     * no longer useful after this method has been called.
751     */
752    public void send() {
753        if (DEBUG) debug("send");
754
755        // Get ready to write to disk.
756        prepareForSave();
757
758        // We need the recipient list for both SMS and MMS.
759        final String[] dests = mRecipients;
760
761        if (requiresMms()) {
762            // Make local copies of the bits we need for sending a message,
763            // because we will be doing it off of the main thread, which will
764            // immediately continue on to resetting some of this state.
765            final Uri mmsUri = mMessageUri;
766            final PduPersister persister = PduPersister.getPduPersister(mContext);
767
768            final SlideshowModel slideshow = mSlideshow;
769            final SendReq sendReq = makeSendReq(dests, mSubject);
770
771            // Make sure the text in slide 0 is no longer holding onto a reference to the text
772            // in the message text box.
773            slideshow.prepareForSend();
774
775            // Do the dirty work of sending the message off of the main UI thread.
776            new Thread(new Runnable() {
777                public void run() {
778                    sendMmsWorker(dests, mmsUri, persister, slideshow, sendReq);
779                }
780            }).start();
781        } else {
782            // Same rules apply as above.
783            final String msgText = mText.toString();
784            new Thread(new Runnable() {
785                public void run() {
786                    sendSmsWorker(dests, msgText);
787                }
788            }).start();
789        }
790
791        // Mark the message as discarded because it is "off the market" after being sent.
792        mDiscarded = true;
793    }
794
795
796    // Message sending stuff
797
798    private void sendSmsWorker(String[] dests, String msgText) {
799        // Make sure we are still using the correct thread ID for our
800        // recipient set.
801        long threadId = getOrCreateThreadId(dests);
802
803        MessageSender sender = new SmsMessageSender(mContext, dests, msgText, threadId);
804        try {
805            sender.sendMessage(threadId);
806        } catch (Exception e) {
807            Log.e(TAG, "Failed to send SMS message, threadId=" + threadId, e);
808        }
809
810        mStatusListener.onMessageSent(threadId);
811    }
812
813    private void sendMmsWorker(String[] dests, Uri mmsUri, PduPersister persister,
814                               SlideshowModel slideshow, SendReq sendReq) {
815        // Make sure we are still using the correct thread ID for our
816        // recipient set.
817        long threadId = getOrCreateThreadId(dests);
818
819        if (DEBUG) debug("sendMmsWorker: update draft MMS message " + mmsUri);
820
821        if (mmsUri == null) {
822            // Create a new MMS message if one hasn't been made yet.
823            mmsUri = createDraftMmsMessage(persister, sendReq, slideshow);
824        } else {
825            // Otherwise, sync the MMS message in progress to disk.
826            updateDraftMmsMessage(mmsUri, persister, slideshow, sendReq);
827        }
828
829        // Be paranoid and clean any draft SMS up.
830        deleteDraftSmsMessage(threadId);
831
832        MessageSender sender = new MmsMessageSender(mContext, mmsUri);
833        try {
834            if (!sender.sendMessage(threadId)) {
835                // The message was sent through SMS protocol, we should
836                // delete the copy which was previously saved in MMS drafts.
837                SqliteWrapper.delete(mContext, mContentResolver, mmsUri, null, null);
838            }
839        } catch (Exception e) {
840            Log.e(TAG, "Failed to send message: " + mmsUri + ", threadId=" + threadId, e);
841        }
842
843        mStatusListener.onMessageSent(threadId);
844    }
845
846    // Draft message stuff
847
848    private static final String[] MMS_DRAFT_PROJECTION = {
849        Mms._ID,        // 0
850        Mms.SUBJECT     // 1
851    };
852
853    private static final int MMS_ID_INDEX       = 0;
854    private static final int MMS_SUBJECT_INDEX  = 1;
855
856    private static Uri readDraftMmsMessage(Context context, long threadId, StringBuilder sb) {
857        if (DEBUG) debug("readDraftMmsMessage tid=%d", threadId);
858        Cursor cursor;
859        ContentResolver cr = context.getContentResolver();
860
861        final String selection = Mms.THREAD_ID + " = " + threadId;
862        cursor = SqliteWrapper.query(context, cr,
863                Mms.Draft.CONTENT_URI, MMS_DRAFT_PROJECTION,
864                selection, null, null);
865
866        Uri uri;
867        try {
868            if (cursor.moveToFirst()) {
869                uri = ContentUris.withAppendedId(Mms.Draft.CONTENT_URI,
870                        cursor.getLong(MMS_ID_INDEX));
871                String subject = cursor.getString(MMS_SUBJECT_INDEX);
872                if (subject != null) {
873                    sb.append(subject);
874                }
875                return uri;
876            }
877        } finally {
878            cursor.close();
879        }
880
881        return null;
882    }
883
884    private long getOrCreateThreadId(String[] numbers) {
885        HashSet<String> recipients = new HashSet<String>();
886        recipients.addAll(Arrays.asList(numbers));
887        long threadId = Threads.getOrCreateThreadId(mContext, recipients);
888        if (DEBUG) debug("getOrCreateThreadId mThreadId=%d, numbers=%s, result=%d", mThreadId, numbers, threadId);
889        return threadId;
890    }
891
892    private static SendReq makeSendReq(String[] dests, CharSequence subject) {
893        SendReq req = new SendReq();
894        EncodedStringValue[] encodedNumbers = EncodedStringValue.encodeStrings(dests);
895        if (encodedNumbers != null) {
896            req.setTo(encodedNumbers);
897        }
898
899        if (!TextUtils.isEmpty(subject)) {
900            req.setSubject(new EncodedStringValue(subject.toString()));
901        }
902
903        req.setDate(System.currentTimeMillis() / 1000L);
904
905        return req;
906    }
907
908    private static Uri createDraftMmsMessage(PduPersister persister, SendReq sendReq,
909            SlideshowModel slideshow) {
910        try {
911            PduBody pb = slideshow.toPduBody();
912            sendReq.setBody(pb);
913            Uri res = persister.persist(sendReq, Mms.Draft.CONTENT_URI);
914            slideshow.sync(pb);
915            return res;
916        } catch (MmsException e) {
917            return null;
918        }
919    }
920
921    private void asyncUpdateDraftMmsMessage(final String[] dests) {
922        if (DEBUG) debug("asyncUpdateDraftMmsMessage dests=%s mMessageUri=%s", dests, mMessageUri);
923        // PduPersister makes database calls and is known to ANR. Do the work on a
924        // background thread.
925        final PduPersister persister = PduPersister.getPduPersister(mContext);
926        final SendReq sendReq = makeSendReq(dests, mSubject);
927
928        new Thread(new Runnable() {
929            public void run() {
930                mThreadId = getOrCreateThreadId(dests);
931                DraftCache.getInstance().setDraftState(mThreadId, true);
932                if (mMessageUri == null) {
933                    mMessageUri = createDraftMmsMessage(persister, sendReq, mSlideshow);
934                } else {
935                    updateDraftMmsMessage(mMessageUri, persister, mSlideshow, sendReq);
936                }
937            }
938        }).start();
939
940        // Be paranoid and delete any SMS drafts that might be lying around.
941        asyncDeleteDraftSmsMessage(mThreadId);
942    }
943
944    private static void updateDraftMmsMessage(Uri uri, PduPersister persister,
945            SlideshowModel slideshow, SendReq sendReq) {
946        if (DEBUG) debug("updateDraftMmsMessage uri=%s", uri);
947        persister.updateHeaders(uri, sendReq);
948        final PduBody pb = slideshow.toPduBody();
949
950        try {
951            persister.updateParts(uri, pb);
952        } catch (MmsException e) {
953            Log.e(TAG, "updateDraftMmsMessage: cannot update message " + uri);
954        }
955
956        slideshow.sync(pb);
957    }
958
959    private static final String SMS_DRAFT_WHERE = Sms.TYPE + "=" + Sms.MESSAGE_TYPE_DRAFT;
960    private static final String[] SMS_BODY_PROJECTION = { Sms._ID, Sms.BODY };
961    private static final int SMS_ID_INDEX = 0;
962    private static final int SMS_BODY_INDEX = 0;
963
964    /**
965     * Reads a draft message for the given thread ID from the database,
966     * if there is one, deletes it from the database, and returns it.
967     * @return The draft message or an empty string.
968     */
969    private static String readDraftSmsMessage(Context context, long thread_id) {
970        if (DEBUG) debug("readDraftSmsMessage tid=%d", thread_id);
971        ContentResolver cr = context.getContentResolver();
972
973        // If it's an invalid thread, don't bother.
974        if (thread_id <= 0) {
975            return "";
976        }
977
978        Uri thread_uri = ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, thread_id);
979        String body = "";
980
981        Cursor c = SqliteWrapper.query(context, cr,
982                        thread_uri, SMS_BODY_PROJECTION, SMS_DRAFT_WHERE, null, null);
983        try {
984            if (c.moveToFirst()) {
985                body = c.getString(SMS_BODY_INDEX);
986            }
987        } finally {
988            c.close();
989        }
990
991        // Clean out drafts for this thread -- if the recipient set changes,
992        // we will lose track of the original draft and be unable to delete
993        // it later.  The message will be re-saved if necessary upon exit of
994        // the activity.
995        SqliteWrapper.delete(context, cr, thread_uri, SMS_DRAFT_WHERE, null);
996
997        return body;
998    }
999
1000    private void asyncUpdateDraftSmsMessage(final String[] dests, final String contents) {
1001        new Thread(new Runnable() {
1002            public void run() {
1003                long oldThreadId = mThreadId;
1004                mThreadId = getOrCreateThreadId(dests);
1005                if (DEBUG) debug("asyncUpdateDraftSmsMessage old=%d tid=%d dests=%s", oldThreadId, mThreadId, dests);
1006                DraftCache.getInstance().setDraftState(mThreadId, true);
1007                updateDraftSmsMessage(mThreadId, contents);
1008            }
1009        }).start();
1010    }
1011
1012    private void updateDraftSmsMessage(long thread_id, String contents) {
1013        if (DEBUG) debug("updateDraftSmsMessage tid=%d, contents=\"%s\"", thread_id, contents);
1014
1015        // If we don't have a valid thread, there's nothing to do.
1016        if (thread_id <= 0) {
1017            return;
1018        }
1019
1020        // Don't bother saving an empty message.
1021        if (TextUtils.isEmpty(contents)) {
1022            // But delete the old draft message if it's there.
1023            deleteDraftSmsMessage(thread_id);
1024            return;
1025        }
1026
1027        Uri thread_uri = ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, thread_id);
1028        Cursor c = SqliteWrapper.query(mContext, mContentResolver,
1029                thread_uri, SMS_BODY_PROJECTION, SMS_DRAFT_WHERE, null, null);
1030
1031        try {
1032            if (c.moveToFirst()) {
1033                ContentValues values = new ContentValues(1);
1034                values.put(Sms.BODY, contents);
1035                SqliteWrapper.update(mContext, mContentResolver, thread_uri, values,
1036                        SMS_DRAFT_WHERE, null);
1037            } else {
1038                ContentValues values = new ContentValues(3);
1039                values.put(Sms.THREAD_ID, thread_id);
1040                values.put(Sms.BODY, contents);
1041                values.put(Sms.TYPE, Sms.MESSAGE_TYPE_DRAFT);
1042                SqliteWrapper.insert(mContext, mContentResolver, Sms.CONTENT_URI, values);
1043                asyncDeleteDraftMmsMessage(thread_id);
1044            }
1045        } finally {
1046            c.close();
1047        }
1048    }
1049
1050    private void asyncDelete(final Uri uri, final String selection, final String[] selectionArgs) {
1051        if (DEBUG) debug("asyncDelete %s where %s", uri, selection);
1052        new Thread(new Runnable() {
1053            public void run() {
1054                SqliteWrapper.delete(mContext, mContentResolver, uri, selection, selectionArgs);
1055            }
1056        }).start();
1057    }
1058
1059    private void asyncDeleteDraftSmsMessage(long threadId) {
1060        if (threadId > 0) {
1061            asyncDelete(ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, threadId),
1062                SMS_DRAFT_WHERE, null);
1063        }
1064    }
1065
1066    private void deleteDraftSmsMessage(long threadId) {
1067        SqliteWrapper.delete(mContext, mContentResolver,
1068                ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, threadId),
1069                SMS_DRAFT_WHERE, null);
1070    }
1071
1072    private void asyncDeleteDraftMmsMessage(long threadId) {
1073        final String where = Mms.THREAD_ID + " = " + threadId;
1074        asyncDelete(Mms.Draft.CONTENT_URI, where, null);
1075    }
1076
1077    // Logging stuff.
1078
1079    private static String prettyArray(String[] array) {
1080        if (array.length == 0) {
1081            return "[]";
1082        }
1083
1084        StringBuilder sb = new StringBuilder("[");
1085        int len = array.length-1;
1086        for (int i = 0; i < len; i++) {
1087            sb.append(array[i]);
1088            sb.append(", ");
1089        }
1090        sb.append(array[len]);
1091        sb.append("]");
1092
1093        return sb.toString();
1094    }
1095
1096    private static String logFormat(String format, Object... args) {
1097        for (int i = 0; i < args.length; i++) {
1098            if (args[i] instanceof String[]) {
1099                args[i] = prettyArray((String[])args[i]);
1100            }
1101        }
1102        String s = String.format(format, args);
1103        s = "[" + Thread.currentThread().getId() + "] " + s;
1104        return s;
1105    }
1106
1107    private static void debug(String format, Object... args) {
1108        Log.d(TAG, logFormat(format, args));
1109    }
1110
1111    private static void warn(String format, Object... args) {
1112        Log.w(TAG, logFormat(format, args));
1113    }
1114
1115    private static void error(String format, Object... args) {
1116        Log.e(TAG, logFormat(format, args));
1117    }
1118}
1119