WorkingMessage.java revision 4ee7aa9aec45dd564c0e82928fa5a8378e856dcf
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        } else if (append) {
361            // We added a new slide and what we attempted to insert on the slide failed.
362            // Delete that slide, otherwise we could end up with a bunch of blank slides.
363            SlideshowEditor slideShowEditor = new SlideshowEditor(mContext, mSlideshow);
364            slideShowEditor.removeSlide(mSlideshow.size() - 1);
365        }
366
367        // Set HAS_ATTACHMENT if we need it.
368        updateState(HAS_ATTACHMENT, hasAttachment(), true);
369
370        return result;
371    }
372
373    /**
374     * Returns true if this message contains anything worth saving.
375     */
376    public boolean isWorthSaving() {
377        // If it actually contains anything, it's of course not empty.
378        if (hasText() || hasSubject() || hasAttachment() || hasSlideshow()) {
379            return true;
380        }
381
382        // When saveAsMms() has been called, we set FORCE_MMS to represent
383        // sort of an "invisible attachment" so that the message isn't thrown
384        // away when we are shipping it off to other activities.
385        if ((mMmsState & FORCE_MMS) > 0) {
386            return true;
387        }
388
389        return false;
390    }
391
392    /**
393     * Makes sure mSlideshow is set up.
394     */
395    private void ensureSlideshow() {
396        if (mSlideshow != null) {
397            return;
398        }
399
400        SlideshowModel slideshow = SlideshowModel.createNew(mContext);
401        SlideModel slide = new SlideModel(slideshow);
402        slideshow.add(slide);
403
404        mSlideshow = slideshow;
405    }
406
407    /**
408     * Change the message's attachment to the data in the specified Uri.
409     * Used only for single-slide ("attachment mode") messages.
410     */
411    private void changeMedia(int type, Uri uri) throws MmsException {
412        SlideModel slide = mSlideshow.get(0);
413        MediaModel media;
414
415        if (slide == null) {
416            Log.w(LogTag.TAG, "[WorkingMessage] changeMedia: no slides!");
417            return;
418        }
419
420        // Remove any previous attachments.
421        slide.removeImage();
422        slide.removeVideo();
423        slide.removeAudio();
424
425        // If we're changing to text, just bail out.
426        if (type == TEXT) {
427            return;
428        }
429
430        // Make a correct MediaModel for the type of attachment.
431        if (type == IMAGE) {
432            media = new ImageModel(mContext, uri, mSlideshow.getLayout().getImageRegion());
433        } else if (type == VIDEO) {
434            media = new VideoModel(mContext, uri, mSlideshow.getLayout().getImageRegion());
435        } else if (type == AUDIO) {
436            media = new AudioModel(mContext, uri);
437        } else {
438            throw new IllegalArgumentException("changeMedia type=" + type + ", uri=" + uri);
439        }
440
441        // Add it to the slide.
442        slide.add(media);
443
444        // For video and audio, set the duration of the slide to
445        // that of the attachment.
446        if (type == VIDEO || type == AUDIO) {
447            slide.updateDuration(media.getDuration());
448        }
449    }
450
451    /**
452     * Add the message's attachment to the data in the specified Uri to a new slide.
453     */
454    private void appendMedia(int type, Uri uri) throws MmsException {
455
456        // If we're changing to text, just bail out.
457        if (type == TEXT) {
458            return;
459        }
460
461        // The first time this method is called, mSlideshow.size() is going to be
462        // one (a newly initialized slideshow has one empty slide). The first time we
463        // attach the picture/video to that first empty slide. From then on when this
464        // function is called, we've got to create a new slide and add the picture/video
465        // to that new slide.
466        boolean addNewSlide = true;
467        if (mSlideshow.size() == 1 && !mSlideshow.isSimple()) {
468            addNewSlide = false;
469        }
470        if (addNewSlide) {
471            SlideshowEditor slideShowEditor = new SlideshowEditor(mContext, mSlideshow);
472            if (!slideShowEditor.addNewSlide()) {
473                return;
474            }
475        }
476        // Make a correct MediaModel for the type of attachment.
477        MediaModel media;
478        SlideModel slide = mSlideshow.get(mSlideshow.size() - 1);
479        if (type == IMAGE) {
480            media = new ImageModel(mContext, uri, mSlideshow.getLayout().getImageRegion());
481        } else if (type == VIDEO) {
482            media = new VideoModel(mContext, uri, mSlideshow.getLayout().getImageRegion());
483        } else if (type == AUDIO) {
484            media = new AudioModel(mContext, uri);
485        } else {
486            throw new IllegalArgumentException("changeMedia type=" + type + ", uri=" + uri);
487        }
488
489        // Add it to the slide.
490        slide.add(media);
491
492        // For video and audio, set the duration of the slide to
493        // that of the attachment.
494        if (type == VIDEO || type == AUDIO) {
495            slide.updateDuration(media.getDuration());
496        }
497    }
498
499    /**
500     * Returns true if the message has an attachment (including slideshows).
501     */
502    public boolean hasAttachment() {
503        return (mAttachmentType > TEXT);
504    }
505
506    /**
507     * Returns the slideshow associated with this message.
508     */
509    public SlideshowModel getSlideshow() {
510        return mSlideshow;
511    }
512
513    /**
514     * Returns true if the message has a real slideshow, as opposed to just
515     * one image attachment, for example.
516     */
517    public boolean hasSlideshow() {
518        return (mAttachmentType == SLIDESHOW);
519    }
520
521    /**
522     * Sets the MMS subject of the message.  Passing null indicates that there
523     * is no subject.  Passing "" will result in an empty subject being added
524     * to the message, possibly triggering a conversion to MMS.  This extra
525     * bit of state is needed to support ComposeMessageActivity converting to
526     * MMS when the user adds a subject.  An empty subject will be removed
527     * before saving to disk or sending, however.
528     */
529    public void setSubject(CharSequence s, boolean notify) {
530        mSubject = s;
531        updateState(HAS_SUBJECT, (s != null), notify);
532    }
533
534    /**
535     * Returns the MMS subject of the message.
536     */
537    public CharSequence getSubject() {
538        return mSubject;
539    }
540
541    /**
542     * Returns true if this message has an MMS subject.
543     * @return
544     */
545    public boolean hasSubject() {
546        return !TextUtils.isEmpty(mSubject);
547    }
548
549    /**
550     * Moves the message text into the slideshow.  Should be called any time
551     * the message is about to be sent or written to disk.
552     */
553    private void syncTextToSlideshow() {
554        if (mSlideshow == null || mSlideshow.size() != 1)
555            return;
556
557        SlideModel slide = mSlideshow.get(0);
558        TextModel text;
559        if (!slide.hasText()) {
560            // Add a TextModel to slide 0 if one doesn't already exist
561            text = new TextModel(mContext, ContentType.TEXT_PLAIN, "text_0.txt",
562                                           mSlideshow.getLayout().getTextRegion());
563            slide.add(text);
564        } else {
565            // Otherwise just reuse the existing one.
566            text = slide.getText();
567        }
568        text.setText(mText);
569    }
570
571    /**
572     * Sets the message text out of the slideshow.  Should be called any time
573     * a slideshow is loaded from disk.
574     */
575    private void syncTextFromSlideshow() {
576        // Don't sync text for real slideshows.
577        if (mSlideshow.size() != 1) {
578            return;
579        }
580
581        SlideModel slide = mSlideshow.get(0);
582        if (slide == null || !slide.hasText()) {
583            return;
584        }
585
586        mText = slide.getText().getText();
587    }
588
589    /**
590     * Removes the subject if it is empty, possibly converting back to SMS.
591     */
592    private void removeSubjectIfEmpty(boolean notify) {
593        if (!hasSubject()) {
594            setSubject(null, notify);
595        }
596    }
597
598    /**
599     * Gets internal message state ready for storage.  Should be called any
600     * time the message is about to be sent or written to disk.
601     */
602    private void prepareForSave(boolean notify) {
603        // Make sure our working set of recipients is resolved
604        // to first-class Contact objects before we save.
605        syncWorkingRecipients();
606
607        if (requiresMms()) {
608            ensureSlideshow();
609            syncTextToSlideshow();
610            removeSubjectIfEmpty(notify);
611        }
612    }
613
614    /**
615     * Resolve the temporary working set of recipients to a ContactList.
616     */
617    public void syncWorkingRecipients() {
618        if (mWorkingRecipients != null) {
619            ContactList recipients = ContactList.getByNumbers(mWorkingRecipients, false);
620            mConversation.setRecipients(recipients);
621            mWorkingRecipients = null;
622        }
623    }
624
625
626    /**
627     * Force the message to be saved as MMS and return the Uri of the message.
628     * Typically used when handing a message off to another activity.
629     */
630    public Uri saveAsMms(boolean notify) {
631        if (DEBUG) LogTag.debug("save mConversation=%s", mConversation);
632
633        if (mDiscarded) {
634            throw new IllegalStateException("save() called after discard()");
635        }
636
637        // FORCE_MMS behaves as sort of an "invisible attachment", making
638        // the message seem non-empty (and thus not discarded).  This bit
639        // is sticky until the last other MMS bit is removed, at which
640        // point the message will fall back to SMS.
641        updateState(FORCE_MMS, true, notify);
642
643        // Collect our state to be written to disk.
644        prepareForSave(true /* notify */);
645
646        // Make sure we are saving to the correct thread ID.
647        mConversation.ensureThreadId();
648        mConversation.setDraftState(true);
649
650        PduPersister persister = PduPersister.getPduPersister(mContext);
651        SendReq sendReq = makeSendReq(mConversation, mSubject);
652
653        // If we don't already have a Uri lying around, make a new one.  If we do
654        // have one already, make sure it is synced to disk.
655        if (mMessageUri == null) {
656            mMessageUri = createDraftMmsMessage(persister, sendReq, mSlideshow);
657        } else {
658            updateDraftMmsMessage(mMessageUri, persister, mSlideshow, sendReq);
659        }
660
661        return mMessageUri;
662    }
663
664    /**
665     * Save this message as a draft in the conversation previously specified
666     * to {@link setConversation}.
667     */
668    public void saveDraft() {
669        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
670            LogTag.debug("saveDraft");
671        }
672
673        // If we have discarded the message, just bail out.
674        if (mDiscarded) {
675            return;
676        }
677
678        // Make sure setConversation was called.
679        if (mConversation == null) {
680            throw new IllegalStateException("saveDraft() called with no conversation");
681        }
682
683        // Get ready to write to disk. But don't notify message status when saving draft
684        prepareForSave(false /* notify */);
685
686        if (requiresMms()) {
687            asyncUpdateDraftMmsMessage(mConversation);
688        } else {
689            String content = mText.toString();
690
691            // bug 2169583: don't bother creating a thread id only to delete the thread
692            // because the content is empty. When we delete the thread in updateDraftSmsMessage,
693            // we didn't nullify conv.mThreadId, causing a temperary situation where conv
694            // is holding onto a thread id that isn't in the database. If a new message arrives
695            // and takes that thread id (because it's the next thread id to be assigned), the
696            // new message will be merged with the draft message thread, causing confusion!
697            if (!TextUtils.isEmpty(content)) {
698                asyncUpdateDraftSmsMessage(mConversation, content);
699            }
700        }
701
702        // Update state of the draft cache.
703        mConversation.setDraftState(true);
704    }
705
706    public void discard() {
707        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
708            LogTag.debug("discard");
709        }
710
711        // Technically, we could probably just bail out here.  But discard() is
712        // really meant to be called if you never want to use the message again,
713        // so keep this assert in as a debugging aid.
714        if (mDiscarded == true) {
715            throw new IllegalStateException("discard() called twice");
716        }
717
718        // Mark this message as discarded in order to make saveDraft() no-op.
719        mDiscarded = true;
720
721        // Delete our MMS message, if there is one.
722        if (mMessageUri != null) {
723            asyncDelete(mMessageUri, null, null);
724        }
725
726        // Delete any draft messages associated with this conversation.
727        asyncDeleteDraftSmsMessage(mConversation);
728
729        // Update state of the draft cache.
730        mConversation.setDraftState(false);
731    }
732
733    public void unDiscard() {
734        if (DEBUG) LogTag.debug("unDiscard");
735
736        mDiscarded = false;
737    }
738
739    /**
740     * Returns true if discard() has been called on this message.
741     */
742    public boolean isDiscarded() {
743        return mDiscarded;
744    }
745
746    /**
747     * To be called from our Activity's onSaveInstanceState() to give us a chance
748     * to stow our state away for later retrieval.
749     *
750     * @param bundle The Bundle passed in to onSaveInstanceState
751     */
752    public void writeStateToBundle(Bundle bundle) {
753        if (hasSubject()) {
754            bundle.putString("subject", mSubject.toString());
755        }
756
757        if (mMessageUri != null) {
758            bundle.putParcelable("msg_uri", mMessageUri);
759        } else if (hasText()) {
760            bundle.putString("sms_body", mText.toString());
761        }
762    }
763
764    /**
765     * To be called from our Activity's onCreate() if the activity manager
766     * has given it a Bundle to reinflate
767     * @param bundle The Bundle passed in to onCreate
768     */
769    public void readStateFromBundle(Bundle bundle) {
770        if (bundle == null) {
771            return;
772        }
773
774        String subject = bundle.getString("subject");
775        setSubject(subject, false);
776
777        Uri uri = (Uri)bundle.getParcelable("msg_uri");
778        if (uri != null) {
779            loadFromUri(uri);
780            return;
781        } else {
782            String body = bundle.getString("sms_body");
783            mText = body;
784        }
785    }
786
787    /**
788     * Update the temporary list of recipients, used when setting up a
789     * new conversation.  Will be converted to a ContactList on any
790     * save event (send, save draft, etc.)
791     */
792    public void setWorkingRecipients(List<String> numbers) {
793        mWorkingRecipients = numbers;
794    }
795
796    /**
797     * Set the conversation associated with this message.
798     */
799    public void setConversation(Conversation conv) {
800        if (DEBUG) LogTag.debug("setConversation %s -> %s", mConversation, conv);
801
802        mConversation = conv;
803
804        // Convert to MMS if there are any email addresses in the recipient list.
805        setHasEmail(conv.getRecipients().containsEmail(), false);
806    }
807
808    /**
809     * Hint whether or not this message will be delivered to an
810     * an email address.
811     */
812    public void setHasEmail(boolean hasEmail, boolean notify) {
813        if (MmsConfig.getEmailGateway() != null) {
814            updateState(RECIPIENTS_REQUIRE_MMS, false, notify);
815        } else {
816            updateState(RECIPIENTS_REQUIRE_MMS, hasEmail, notify);
817        }
818    }
819
820    /**
821     * Returns true if this message would require MMS to send.
822     */
823    public boolean requiresMms() {
824        return (mMmsState > 0);
825    }
826
827    /**
828     * Set whether or not we want to send this message via MMS in order to
829     * avoid sending an excessive number of concatenated SMS messages.
830     */
831    public void setLengthRequiresMms(boolean mmsRequired) {
832        updateState(LENGTH_REQUIRES_MMS, mmsRequired, true);
833    }
834
835    private static String stateString(int state) {
836        if (state == 0)
837            return "<none>";
838
839        StringBuilder sb = new StringBuilder();
840        if ((state & RECIPIENTS_REQUIRE_MMS) > 0)
841            sb.append("RECIPIENTS_REQUIRE_MMS | ");
842        if ((state & HAS_SUBJECT) > 0)
843            sb.append("HAS_SUBJECT | ");
844        if ((state & HAS_ATTACHMENT) > 0)
845            sb.append("HAS_ATTACHMENT | ");
846        if ((state & LENGTH_REQUIRES_MMS) > 0)
847            sb.append("LENGTH_REQUIRES_MMS | ");
848        if ((state & FORCE_MMS) > 0)
849            sb.append("FORCE_MMS | ");
850
851        sb.delete(sb.length() - 3, sb.length());
852        return sb.toString();
853    }
854
855    /**
856     * Sets the current state of our various "MMS required" bits.
857     *
858     * @param state The bit to change, such as {@link HAS_ATTACHMENT}
859     * @param on If true, set it; if false, clear it
860     * @param notify Whether or not to notify the user
861     */
862    private void updateState(int state, boolean on, boolean notify) {
863        int oldState = mMmsState;
864        if (on) {
865            mMmsState |= state;
866        } else {
867            mMmsState &= ~state;
868        }
869
870        // If we are clearing the last bit that is not FORCE_MMS,
871        // expire the FORCE_MMS bit.
872        if (mMmsState == FORCE_MMS && ((oldState & ~FORCE_MMS) > 0)) {
873            mMmsState = 0;
874        }
875
876        // Notify the listener if we are moving from SMS to MMS
877        // or vice versa.
878        if (notify) {
879            if (oldState == 0 && mMmsState != 0) {
880                mStatusListener.onProtocolChanged(true);
881            } else if (oldState != 0 && mMmsState == 0) {
882                mStatusListener.onProtocolChanged(false);
883            }
884        }
885
886        if (oldState != mMmsState) {
887            if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) LogTag.debug("updateState: %s%s = %s",
888                    on ? "+" : "-",
889                    stateString(state), stateString(mMmsState));
890        }
891    }
892
893    /**
894     * Send this message over the network.  Will call back with onMessageSent() once
895     * it has been dispatched to the telephony stack.  This WorkingMessage object is
896     * no longer useful after this method has been called.
897     */
898    public void send() {
899        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
900            LogTag.debug("send");
901        }
902
903        // Get ready to write to disk.
904        prepareForSave(true /* notify */);
905
906        // We need the recipient list for both SMS and MMS.
907        final Conversation conv = mConversation;
908        String msgTxt = mText.toString();
909
910        if (requiresMms() || addressContainsEmailToMms(conv, msgTxt)) {
911            // Make local copies of the bits we need for sending a message,
912            // because we will be doing it off of the main thread, which will
913            // immediately continue on to resetting some of this state.
914            final Uri mmsUri = mMessageUri;
915            final PduPersister persister = PduPersister.getPduPersister(mContext);
916
917            final SlideshowModel slideshow = mSlideshow;
918            final SendReq sendReq = makeSendReq(conv, mSubject);
919
920            // Make sure the text in slide 0 is no longer holding onto a reference to the text
921            // in the message text box.
922            slideshow.prepareForSend();
923
924            // Do the dirty work of sending the message off of the main UI thread.
925            new Thread(new Runnable() {
926                public void run() {
927                    sendMmsWorker(conv, mmsUri, persister, slideshow, sendReq);
928                }
929            }).start();
930        } else {
931            // Same rules apply as above.
932            final String msgText = mText.toString();
933            new Thread(new Runnable() {
934                public void run() {
935                    sendSmsWorker(conv, msgText);
936                }
937            }).start();
938        }
939
940        // update the Recipient cache with the new to address, if it's different
941        RecipientIdCache.updateNumbers(conv.getThreadId(), conv.getRecipients());
942
943        // Mark the message as discarded because it is "off the market" after being sent.
944        mDiscarded = true;
945    }
946
947    private boolean addressContainsEmailToMms(Conversation conv, String text) {
948        if (MmsConfig.getEmailGateway() != null) {
949            String[] dests = conv.getRecipients().getNumbers();
950            int length = dests.length;
951            for (int i = 0; i < length; i++) {
952                if (Mms.isEmailAddress(dests[i]) || MessageUtils.isAlias(dests[i])) {
953                    String mtext = dests[i] + " " + text;
954                    int[] params = SmsMessage.calculateLength(mtext, false);
955                    if (params[0] > 1) {
956                        updateState(RECIPIENTS_REQUIRE_MMS, true, true);
957                        ensureSlideshow();
958                        syncTextToSlideshow();
959                        return true;
960                    }
961                }
962            }
963        }
964        return false;
965    }
966
967    // Message sending stuff
968
969    private void sendSmsWorker(Conversation conv, String msgText) {
970        mStatusListener.onPreMessageSent();
971        // Make sure we are still using the correct thread ID for our
972        // recipient set.
973        long threadId = conv.ensureThreadId();
974        String[] dests = conv.getRecipients().getNumbers();
975
976        MessageSender sender = new SmsMessageSender(mContext, dests, msgText, threadId);
977        try {
978            sender.sendMessage(threadId);
979
980            // Make sure this thread isn't over the limits in message count
981            Recycler.getSmsRecycler().deleteOldMessagesByThreadId(mContext, threadId);
982       } catch (Exception e) {
983            Log.e(TAG, "Failed to send SMS message, threadId=" + threadId, e);
984        }
985
986        mStatusListener.onMessageSent();
987    }
988
989    private void sendMmsWorker(Conversation conv, Uri mmsUri, PduPersister persister,
990                               SlideshowModel slideshow, SendReq sendReq) {
991        // First make sure we don't have too many outstanding unsent message.
992        Cursor cursor = null;
993        try {
994            cursor = SqliteWrapper.query(mContext, mContentResolver,
995                    Mms.Outbox.CONTENT_URI, MMS_OUTBOX_PROJECTION, null, null, null);
996            if (cursor != null) {
997                long maxMessageSize = MmsConfig.getMaxSizeScaleForPendingMmsAllowed() *
998                    MmsConfig.getMaxMessageSize();
999                long totalPendingSize = 0;
1000                while (cursor.moveToNext()) {
1001                    totalPendingSize += cursor.getLong(MMS_MESSAGE_SIZE_INDEX);
1002                }
1003                if (totalPendingSize >= maxMessageSize) {
1004                    unDiscard();    // it wasn't successfully sent. Allow it to be saved as a draft.
1005                    mStatusListener.onMaxPendingMessagesReached();
1006                    return;
1007                }
1008            }
1009        } finally {
1010            if (cursor != null) {
1011                cursor.close();
1012            }
1013        }
1014        mStatusListener.onPreMessageSent();
1015
1016        // Make sure we are still using the correct thread ID for our
1017        // recipient set.
1018        long threadId = conv.ensureThreadId();
1019
1020        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1021            LogTag.debug("sendMmsWorker: update draft MMS message " + mmsUri);
1022        }
1023
1024        if (mmsUri == null) {
1025            // Create a new MMS message if one hasn't been made yet.
1026            mmsUri = createDraftMmsMessage(persister, sendReq, slideshow);
1027        } else {
1028            // Otherwise, sync the MMS message in progress to disk.
1029            updateDraftMmsMessage(mmsUri, persister, slideshow, sendReq);
1030        }
1031
1032        // Be paranoid and clean any draft SMS up.
1033        deleteDraftSmsMessage(threadId);
1034
1035        MessageSender sender = new MmsMessageSender(mContext, mmsUri,
1036                slideshow.getCurrentMessageSize());
1037        try {
1038            if (!sender.sendMessage(threadId)) {
1039                // The message was sent through SMS protocol, we should
1040                // delete the copy which was previously saved in MMS drafts.
1041                SqliteWrapper.delete(mContext, mContentResolver, mmsUri, null, null);
1042            }
1043
1044            // Make sure this thread isn't over the limits in message count
1045            Recycler.getMmsRecycler().deleteOldMessagesByThreadId(mContext, threadId);
1046        } catch (Exception e) {
1047            Log.e(TAG, "Failed to send message: " + mmsUri + ", threadId=" + threadId, e);
1048        }
1049
1050        mStatusListener.onMessageSent();
1051    }
1052
1053    // Draft message stuff
1054
1055    private static final String[] MMS_DRAFT_PROJECTION = {
1056        Mms._ID,        // 0
1057        Mms.SUBJECT     // 1
1058    };
1059
1060    private static final int MMS_ID_INDEX       = 0;
1061    private static final int MMS_SUBJECT_INDEX  = 1;
1062
1063    private static Uri readDraftMmsMessage(Context context, long threadId, StringBuilder sb) {
1064        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1065            LogTag.debug("readDraftMmsMessage tid=%d", threadId);
1066        }
1067        Cursor cursor;
1068        ContentResolver cr = context.getContentResolver();
1069
1070        final String selection = Mms.THREAD_ID + " = " + threadId;
1071        cursor = SqliteWrapper.query(context, cr,
1072                Mms.Draft.CONTENT_URI, MMS_DRAFT_PROJECTION,
1073                selection, null, null);
1074
1075        Uri uri;
1076        try {
1077            if (cursor.moveToFirst()) {
1078                uri = ContentUris.withAppendedId(Mms.Draft.CONTENT_URI,
1079                        cursor.getLong(MMS_ID_INDEX));
1080                String subject = cursor.getString(MMS_SUBJECT_INDEX);
1081                if (subject != null) {
1082                    sb.append(subject);
1083                }
1084                return uri;
1085            }
1086        } finally {
1087            cursor.close();
1088        }
1089
1090        return null;
1091    }
1092
1093    /**
1094     * makeSendReq should always return a non-null SendReq, whether the dest addresses are
1095     * valid or not.
1096     */
1097    private static SendReq makeSendReq(Conversation conv, CharSequence subject) {
1098        String[] dests = conv.getRecipients().getNumbers(true /* scrub for MMS address */);
1099
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
1220        return body;
1221    }
1222
1223    private void asyncUpdateDraftSmsMessage(final Conversation conv, final String contents) {
1224        new Thread(new Runnable() {
1225            public void run() {
1226                long threadId = conv.ensureThreadId();
1227                conv.setDraftState(true);
1228                updateDraftSmsMessage(threadId, contents);
1229            }
1230        }).start();
1231    }
1232
1233    private void updateDraftSmsMessage(long thread_id, String contents) {
1234        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1235            LogTag.debug("updateDraftSmsMessage tid=%d, contents=\"%s\"", thread_id, contents);
1236        }
1237
1238        // If we don't have a valid thread, there's nothing to do.
1239        if (thread_id <= 0) {
1240            return;
1241        }
1242
1243        ContentValues values = new ContentValues(3);
1244        values.put(Sms.THREAD_ID, thread_id);
1245        values.put(Sms.BODY, contents);
1246        values.put(Sms.TYPE, Sms.MESSAGE_TYPE_DRAFT);
1247        SqliteWrapper.insert(mContext, mContentResolver, Sms.CONTENT_URI, values);
1248        asyncDeleteDraftMmsMessage(thread_id);
1249    }
1250
1251    private void asyncDelete(final Uri uri, final String selection, final String[] selectionArgs) {
1252        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1253            LogTag.debug("asyncDelete %s where %s", uri, selection);
1254        }
1255        new Thread(new Runnable() {
1256            public void run() {
1257                SqliteWrapper.delete(mContext, mContentResolver, uri, selection, selectionArgs);
1258            }
1259        }).start();
1260    }
1261
1262    private void asyncDeleteDraftSmsMessage(Conversation conv) {
1263        long threadId = conv.getThreadId();
1264        if (threadId > 0) {
1265            asyncDelete(ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, threadId),
1266                SMS_DRAFT_WHERE, null);
1267        }
1268    }
1269
1270    private void deleteDraftSmsMessage(long threadId) {
1271        SqliteWrapper.delete(mContext, mContentResolver,
1272                ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, threadId),
1273                SMS_DRAFT_WHERE, null);
1274    }
1275
1276    private void asyncDeleteDraftMmsMessage(long threadId) {
1277        final String where = Mms.THREAD_ID + " = " + threadId;
1278        asyncDelete(Mms.Draft.CONTENT_URI, where, null);
1279    }
1280
1281}
1282