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