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