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