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