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