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.io.IOException;
20import java.io.InputStream;
21import java.util.ArrayList;
22import java.util.Arrays;
23import java.util.HashMap;
24import java.util.Iterator;
25import java.util.List;
26import java.util.Set;
27
28import android.app.Activity;
29import android.content.ContentResolver;
30import android.content.ContentUris;
31import android.content.ContentValues;
32import android.content.Context;
33import android.database.Cursor;
34import android.database.sqlite.SqliteWrapper;
35import android.net.Uri;
36import android.os.AsyncTask;
37import android.os.Bundle;
38import android.provider.Telephony.Mms;
39import android.provider.Telephony.MmsSms;
40import android.provider.Telephony.MmsSms.PendingMessages;
41import android.provider.Telephony.Sms;
42import android.telephony.SmsMessage;
43import android.text.TextUtils;
44import android.util.Log;
45import android.util.Pair;
46
47import com.android.common.contacts.DataUsageStatUpdater;
48import com.android.common.userhappiness.UserHappinessSignals;
49import com.android.mms.ContentRestrictionException;
50import com.android.mms.ExceedMessageSizeException;
51import com.android.mms.LogTag;
52import com.android.mms.MmsApp;
53import com.android.mms.MmsConfig;
54import com.android.mms.ResolutionException;
55import com.android.mms.UnsupportContentTypeException;
56import com.android.mms.model.ImageModel;
57import com.android.mms.model.SlideModel;
58import com.android.mms.model.SlideshowModel;
59import com.android.mms.model.TextModel;
60import com.android.mms.transaction.MessageSender;
61import com.android.mms.transaction.MmsMessageSender;
62import com.android.mms.transaction.SmsMessageSender;
63import com.android.mms.ui.ComposeMessageActivity;
64import com.android.mms.ui.MessageUtils;
65import com.android.mms.ui.MessagingPreferenceActivity;
66import com.android.mms.ui.SlideshowEditor;
67import com.android.mms.util.DraftCache;
68import com.android.mms.util.Recycler;
69import com.android.mms.util.ThumbnailManager;
70import com.android.mms.widget.MmsWidgetProvider;
71import com.google.android.mms.ContentType;
72import com.google.android.mms.MmsException;
73import com.google.android.mms.pdu.EncodedStringValue;
74import com.google.android.mms.pdu.PduBody;
75import com.google.android.mms.pdu.PduHeaders;
76import com.google.android.mms.pdu.PduPersister;
77import com.google.android.mms.pdu.SendReq;
78
79/**
80 * Contains all state related to a message being edited by the user.
81 */
82public class WorkingMessage {
83    private static final String TAG = "WorkingMessage";
84    private static final boolean DEBUG = false;
85
86    // Public intents
87    public static final String ACTION_SENDING_SMS = "android.intent.action.SENDING_SMS";
88
89    // Intent extras
90    public static final String EXTRA_SMS_MESSAGE = "android.mms.extra.MESSAGE";
91    public static final String EXTRA_SMS_RECIPIENTS = "android.mms.extra.RECIPIENTS";
92    public static final String EXTRA_SMS_THREAD_ID = "android.mms.extra.THREAD_ID";
93
94    // Database access stuff
95    private final Activity mActivity;
96    private final ContentResolver mContentResolver;
97
98    // States that can require us to save or send a message as MMS.
99    private static final int RECIPIENTS_REQUIRE_MMS = (1 << 0);     // 1
100    private static final int HAS_SUBJECT = (1 << 1);                // 2
101    private static final int HAS_ATTACHMENT = (1 << 2);             // 4
102    private static final int LENGTH_REQUIRES_MMS = (1 << 3);        // 8
103    private static final int FORCE_MMS = (1 << 4);                  // 16
104    private static final int MULTIPLE_RECIPIENTS = (1 << 5);        // 32
105
106    // A bitmap of the above indicating different properties of the message;
107    // any bit set will require the message to be sent via MMS.
108    private int mMmsState;
109
110    // Errors from setAttachment()
111    public static final int OK = 0;
112    public static final int UNKNOWN_ERROR = -1;
113    public static final int MESSAGE_SIZE_EXCEEDED = -2;
114    public static final int UNSUPPORTED_TYPE = -3;
115    public static final int IMAGE_TOO_LARGE = -4;
116
117    // Attachment types
118    public static final int TEXT = 0;
119    public static final int IMAGE = 1;
120    public static final int VIDEO = 2;
121    public static final int AUDIO = 3;
122    public static final int SLIDESHOW = 4;
123
124    // Current attachment type of the message; one of the above values.
125    private int mAttachmentType;
126
127    // Conversation this message is targeting.
128    private Conversation mConversation;
129
130    // Text of the message.
131    private CharSequence mText;
132    // Slideshow for this message, if applicable.  If it's a simple attachment,
133    // i.e. not SLIDESHOW, it will contain only one slide.
134    private SlideshowModel mSlideshow;
135    // Data URI of an MMS message if we have had to save it.
136    private Uri mMessageUri;
137    // MMS subject line for this message
138    private CharSequence mSubject;
139
140    // Set to true if this message has been discarded.
141    private boolean mDiscarded = false;
142
143    // Track whether we have drafts
144    private volatile boolean mHasMmsDraft;
145    private volatile boolean mHasSmsDraft;
146
147    // Cached value of mms enabled flag
148    private static boolean sMmsEnabled = MmsConfig.getMmsEnabled();
149
150    // Our callback interface
151    private final MessageStatusListener mStatusListener;
152    private List<String> mWorkingRecipients;
153
154    // Message sizes in Outbox
155    private static final String[] MMS_OUTBOX_PROJECTION = {
156        Mms._ID,            // 0
157        Mms.MESSAGE_SIZE    // 1
158    };
159
160    private static final int MMS_MESSAGE_SIZE_INDEX  = 1;
161
162    /**
163     * Callback interface for communicating important state changes back to
164     * ComposeMessageActivity.
165     */
166    public interface MessageStatusListener {
167        /**
168         * Called when the protocol for sending the message changes from SMS
169         * to MMS, and vice versa.
170         *
171         * @param mms If true, it changed to MMS.  If false, to SMS.
172         */
173        void onProtocolChanged(boolean mms);
174
175        /**
176         * Called when an attachment on the message has changed.
177         */
178        void onAttachmentChanged();
179
180        /**
181         * Called just before the process of sending a message.
182         */
183        void onPreMessageSent();
184
185        /**
186         * Called once the process of sending a message, triggered by
187         * {@link send} has completed. This doesn't mean the send succeeded,
188         * just that it has been dispatched to the network.
189         */
190        void onMessageSent();
191
192        /**
193         * Called if there are too many unsent messages in the queue and we're not allowing
194         * any more Mms's to be sent.
195         */
196        void onMaxPendingMessagesReached();
197
198        /**
199         * Called if there's an attachment error while resizing the images just before sending.
200         */
201        void onAttachmentError(int error);
202    }
203
204    private WorkingMessage(ComposeMessageActivity activity) {
205        mActivity = activity;
206        mContentResolver = mActivity.getContentResolver();
207        mStatusListener = activity;
208        mAttachmentType = TEXT;
209        mText = "";
210    }
211
212    /**
213     * Creates a new working message.
214     */
215    public static WorkingMessage createEmpty(ComposeMessageActivity activity) {
216        // Make a new empty working message.
217        WorkingMessage msg = new WorkingMessage(activity);
218        return msg;
219    }
220
221    /**
222     * Create a new WorkingMessage from the specified data URI, which typically
223     * contains an MMS message.
224     */
225    public static WorkingMessage load(ComposeMessageActivity activity, Uri uri) {
226        // If the message is not already in the draft box, move it there.
227        if (!uri.toString().startsWith(Mms.Draft.CONTENT_URI.toString())) {
228            PduPersister persister = PduPersister.getPduPersister(activity);
229            if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
230                LogTag.debug("load: moving %s to drafts", uri);
231            }
232            try {
233                uri = persister.move(uri, Mms.Draft.CONTENT_URI);
234            } catch (MmsException e) {
235                LogTag.error("Can't move %s to drafts", uri);
236                return null;
237            }
238        }
239
240        WorkingMessage msg = new WorkingMessage(activity);
241        if (msg.loadFromUri(uri)) {
242            msg.mHasMmsDraft = true;
243            return msg;
244        }
245
246        return null;
247    }
248
249    private void correctAttachmentState() {
250        int slideCount = mSlideshow.size();
251
252        // If we get an empty slideshow, tear down all MMS
253        // state and discard the unnecessary message Uri.
254        if (slideCount == 0) {
255            removeAttachment(false);
256        } else if (slideCount > 1) {
257            mAttachmentType = SLIDESHOW;
258        } else {
259            SlideModel slide = mSlideshow.get(0);
260            if (slide.hasImage()) {
261                mAttachmentType = IMAGE;
262            } else if (slide.hasVideo()) {
263                mAttachmentType = VIDEO;
264            } else if (slide.hasAudio()) {
265                mAttachmentType = AUDIO;
266            }
267        }
268
269        updateState(HAS_ATTACHMENT, hasAttachment(), false);
270    }
271
272    private boolean loadFromUri(Uri uri) {
273        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) LogTag.debug("loadFromUri %s", uri);
274        try {
275            mSlideshow = SlideshowModel.createFromMessageUri(mActivity, uri);
276        } catch (MmsException e) {
277            LogTag.error("Couldn't load URI %s", uri);
278            return false;
279        }
280
281        mMessageUri = uri;
282
283        // Make sure all our state is as expected.
284        syncTextFromSlideshow();
285        correctAttachmentState();
286
287        return true;
288    }
289
290    /**
291     * Load the draft message for the specified conversation, or a new empty message if
292     * none exists.
293     */
294    public static WorkingMessage loadDraft(ComposeMessageActivity activity,
295                                           final Conversation conv,
296                                           final Runnable onDraftLoaded) {
297        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) LogTag.debug("loadDraft %s", conv);
298
299        final WorkingMessage msg = createEmpty(activity);
300        if (conv.getThreadId() <= 0) {
301            if (onDraftLoaded != null) {
302                onDraftLoaded.run();
303            }
304            return msg;
305        }
306
307        new AsyncTask<Void, Void, Pair<String, String>>() {
308
309            // Return a Pair where:
310            //    first - non-empty String representing the text of an SMS draft
311            //    second - non-null String representing the text of an MMS subject
312            @Override
313            protected Pair<String, String> doInBackground(Void... none) {
314                // Look for an SMS draft first.
315                String draftText = msg.readDraftSmsMessage(conv);
316                String subject = null;
317
318                if (TextUtils.isEmpty(draftText)) {
319                    // No SMS draft so look for an MMS draft.
320                    StringBuilder sb = new StringBuilder();
321                    Uri uri = readDraftMmsMessage(msg.mActivity, conv, sb);
322                    if (uri != null) {
323                        if (msg.loadFromUri(uri)) {
324                            // If there was an MMS message, readDraftMmsMessage
325                            // will put the subject in our supplied StringBuilder.
326                            subject = sb.toString();
327                        }
328                    }
329                }
330                Pair<String, String> result = new Pair<String, String>(draftText, subject);
331                return result;
332            }
333
334            @Override
335            protected void onPostExecute(Pair<String, String> result) {
336                if (!TextUtils.isEmpty(result.first)) {
337                    msg.mHasSmsDraft = true;
338                    msg.setText(result.first);
339                }
340                if (result.second != null) {
341                    msg.mHasMmsDraft = true;
342                    if (!TextUtils.isEmpty(result.second)) {
343                        msg.setSubject(result.second, false);
344                    }
345                }
346                if (onDraftLoaded != null) {
347                    onDraftLoaded.run();
348                }
349            }
350        }.execute();
351
352        return msg;
353    }
354
355    /**
356     * Sets the text of the message to the specified CharSequence.
357     */
358    public void setText(CharSequence s) {
359        mText = s;
360    }
361
362    /**
363     * Returns the current message text.
364     */
365    public CharSequence getText() {
366        return mText;
367    }
368
369    /**
370     * @return True if the message has any text. A message with just whitespace is not considered
371     * to have text.
372     */
373    public boolean hasText() {
374        return mText != null && TextUtils.getTrimmedLength(mText) > 0;
375    }
376
377    public void removeAttachment(boolean notify) {
378        removeThumbnailsFromCache(mSlideshow);
379        mAttachmentType = TEXT;
380        mSlideshow = null;
381        if (mMessageUri != null) {
382            asyncDelete(mMessageUri, null, null);
383            mMessageUri = null;
384        }
385        // mark this message as no longer having an attachment
386        updateState(HAS_ATTACHMENT, false, notify);
387        if (notify) {
388            // Tell ComposeMessageActivity (or other listener) that the attachment has changed.
389            // In the case of ComposeMessageActivity, it will remove its attachment panel because
390            // this working message no longer has an attachment.
391            mStatusListener.onAttachmentChanged();
392        }
393    }
394
395    public static void removeThumbnailsFromCache(SlideshowModel slideshow) {
396        if (slideshow != null) {
397            ThumbnailManager thumbnailManager = MmsApp.getApplication().getThumbnailManager();
398            boolean removedSomething = false;
399            Iterator<SlideModel> iterator = slideshow.iterator();
400            while (iterator.hasNext()) {
401                SlideModel slideModel = iterator.next();
402                if (slideModel.hasImage()) {
403                    thumbnailManager.removeThumbnail(slideModel.getImage().getUri());
404                    removedSomething = true;
405                } else if (slideModel.hasVideo()) {
406                    thumbnailManager.removeThumbnail(slideModel.getVideo().getUri());
407                    removedSomething = true;
408                }
409            }
410            if (removedSomething) {
411                // HACK: the keys to the thumbnail cache are the part uris, such as mms/part/3
412                // Because the part table doesn't have auto-increment ids, the part ids are reused
413                // when a message or thread is deleted. For now, we're clearing the whole thumbnail
414                // cache so we don't retrieve stale images when part ids are reused. This will be
415                // fixed in the next release in the mms provider.
416                MmsApp.getApplication().getThumbnailManager().clearBackingStore();
417            }
418        }
419    }
420
421    /**
422     * Adds an attachment to the message, replacing an old one if it existed.
423     * @param type Type of this attachment, such as {@link IMAGE}
424     * @param dataUri Uri containing the attachment data (or null for {@link TEXT})
425     * @param append true if we should add the attachment to a new slide
426     * @return An error code such as {@link UNKNOWN_ERROR} or {@link OK} if successful
427     */
428    public int setAttachment(int type, Uri dataUri, boolean append) {
429        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
430            LogTag.debug("setAttachment type=%d uri %s", type, dataUri);
431        }
432        int result = OK;
433        SlideshowEditor slideShowEditor = new SlideshowEditor(mActivity, mSlideshow);
434
435        // Special case for deleting a slideshow. When ComposeMessageActivity gets told to
436        // remove an attachment (search for AttachmentEditor.MSG_REMOVE_ATTACHMENT), it calls
437        // this function setAttachment with a type of TEXT and a null uri. Basically, it's turning
438        // the working message from an MMS back to a simple SMS. The various attachment types
439        // use slide[0] as a special case. The call to ensureSlideshow below makes sure there's
440        // a slide zero. In the case of an already attached slideshow, ensureSlideshow will do
441        // nothing and the slideshow will remain such that if a user adds a slideshow again, they'll
442        // see their old slideshow they previously deleted. Here we really delete the slideshow.
443        if (type == TEXT && mAttachmentType == SLIDESHOW && mSlideshow != null && dataUri == null
444                && !append) {
445            slideShowEditor.removeAllSlides();
446        }
447
448        // Make sure mSlideshow is set up and has a slide.
449        ensureSlideshow();      // mSlideshow can be null before this call, won't be afterwards
450        slideShowEditor.setSlideshow(mSlideshow);
451
452        // Change the attachment
453        result = append ? appendMedia(type, dataUri, slideShowEditor)
454                : changeMedia(type, dataUri, slideShowEditor);
455
456        // If we were successful, update mAttachmentType and notify
457        // the listener than there was a change.
458        if (result == OK) {
459            mAttachmentType = type;
460        }
461        correctAttachmentState();   // this can remove the slideshow if there are no attachments
462
463        if (mSlideshow != null && type == IMAGE) {
464            // Prime the image's cache; helps A LOT when the image is coming from the network
465            // (e.g. Picasa album). See b/5445690.
466            int numSlides = mSlideshow.size();
467            if (numSlides > 0) {
468                ImageModel imgModel = mSlideshow.get(numSlides - 1).getImage();
469                if (imgModel != null) {
470                    cancelThumbnailLoading();
471                    imgModel.loadThumbnailBitmap(null);
472                }
473            }
474        }
475
476        mStatusListener.onAttachmentChanged();  // have to call whether succeeded or failed,
477                                                // because a replace that fails, removes the slide
478
479        if (!append && mAttachmentType == TEXT && type == TEXT) {
480            int[] params = SmsMessage.calculateLength(getText(), false);
481            /* SmsMessage.calculateLength returns an int[4] with:
482             *   int[0] being the number of SMS's required,
483             *   int[1] the number of code units used,
484             *   int[2] is the number of code units remaining until the next message.
485             *   int[3] is the encoding type that should be used for the message.
486             */
487            int smsSegmentCount = params[0];
488
489            if (!MmsConfig.getMultipartSmsEnabled()) {
490                // The provider doesn't support multi-part sms's so as soon as the user types
491                // an sms longer than one segment, we have to turn the message into an mms.
492                setLengthRequiresMms(smsSegmentCount > 1, false);
493            } else {
494                int threshold = MmsConfig.getSmsToMmsTextThreshold();
495                setLengthRequiresMms(threshold > 0 && smsSegmentCount > threshold, false);
496            }
497        } else {
498            // Set HAS_ATTACHMENT if we need it.
499            updateState(HAS_ATTACHMENT, hasAttachment(), true);
500        }
501        return result;
502    }
503
504    /**
505     * Returns true if this message contains anything worth saving.
506     */
507    public boolean isWorthSaving() {
508        // If it actually contains anything, it's of course not empty.
509        if (hasText() || hasSubject() || hasAttachment() || hasSlideshow()) {
510            return true;
511        }
512
513        // When saveAsMms() has been called, we set FORCE_MMS to represent
514        // sort of an "invisible attachment" so that the message isn't thrown
515        // away when we are shipping it off to other activities.
516        if (isFakeMmsForDraft()) {
517            return true;
518        }
519
520        return false;
521    }
522
523    private void cancelThumbnailLoading() {
524        int numSlides = mSlideshow != null ? mSlideshow.size() : 0;
525        if (numSlides > 0) {
526            ImageModel imgModel = mSlideshow.get(numSlides - 1).getImage();
527            if (imgModel != null) {
528                imgModel.cancelThumbnailLoading();
529            }
530        }
531    }
532
533    /**
534     * Returns true if FORCE_MMS is set.
535     * When saveAsMms() has been called, we set FORCE_MMS to represent
536     * sort of an "invisible attachment" so that the message isn't thrown
537     * away when we are shipping it off to other activities.
538     */
539    public boolean isFakeMmsForDraft() {
540        return (mMmsState & FORCE_MMS) > 0;
541    }
542
543    /**
544     * Makes sure mSlideshow is set up.
545     */
546    private void ensureSlideshow() {
547        if (mSlideshow != null) {
548            return;
549        }
550
551        SlideshowModel slideshow = SlideshowModel.createNew(mActivity);
552        SlideModel slide = new SlideModel(slideshow);
553        slideshow.add(slide);
554
555        mSlideshow = slideshow;
556    }
557
558    /**
559     * Change the message's attachment to the data in the specified Uri.
560     * Used only for single-slide ("attachment mode") messages. If the attachment fails to
561     * attach, restore the slide to its original state.
562     */
563    private int changeMedia(int type, Uri uri, SlideshowEditor slideShowEditor) {
564        SlideModel originalSlide = mSlideshow.get(0);
565        if (originalSlide != null) {
566            slideShowEditor.removeSlide(0);     // remove the original slide
567        }
568        slideShowEditor.addNewSlide(0);
569        SlideModel slide = mSlideshow.get(0);   // get the new empty slide
570        int result = OK;
571
572        if (slide == null) {
573            Log.w(LogTag.TAG, "[WorkingMessage] changeMedia: no slides!");
574            return result;
575        }
576
577        // Clear the attachment type since we removed all the attachments. If this isn't cleared
578        // and the slide.add fails (for instance, a selected video could be too big), we'll be
579        // left in a state where we think we have an attachment, but it's been removed from the
580        // slide.
581        mAttachmentType = TEXT;
582
583        // If we're changing to text, just bail out.
584        if (type == TEXT) {
585            return result;
586        }
587
588        result = internalChangeMedia(type, uri, 0, slideShowEditor);
589        if (result != OK) {
590            slideShowEditor.removeSlide(0);             // remove the failed slide
591            if (originalSlide != null) {
592                slideShowEditor.addSlide(0, originalSlide); // restore the original slide.
593            }
594        }
595        return result;
596    }
597
598    /**
599     * Add the message's attachment to the data in the specified Uri to a new slide.
600     */
601    private int appendMedia(int type, Uri uri, SlideshowEditor slideShowEditor) {
602        int result = OK;
603
604        // If we're changing to text, just bail out.
605        if (type == TEXT) {
606            return result;
607        }
608
609        // The first time this method is called, mSlideshow.size() is going to be
610        // one (a newly initialized slideshow has one empty slide). The first time we
611        // attach the picture/video to that first empty slide. From then on when this
612        // function is called, we've got to create a new slide and add the picture/video
613        // to that new slide.
614        boolean addNewSlide = true;
615        if (mSlideshow.size() == 1 && !mSlideshow.isSimple()) {
616            addNewSlide = false;
617        }
618        if (addNewSlide) {
619            if (!slideShowEditor.addNewSlide()) {
620                return result;
621            }
622        }
623        int slideNum = mSlideshow.size() - 1;
624        result = internalChangeMedia(type, uri, slideNum, slideShowEditor);
625        if (result != OK) {
626            // We added a new slide and what we attempted to insert on the slide failed.
627            // Delete that slide, otherwise we could end up with a bunch of blank slides.
628            // It's ok that we're removing the slide even if we didn't add it (because it was
629            // the first default slide). If adding the first slide fails, we want to remove it.
630            slideShowEditor.removeSlide(slideNum);
631        }
632        return result;
633    }
634
635    private int internalChangeMedia(int type, Uri uri, int slideNum,
636            SlideshowEditor slideShowEditor) {
637        int result = OK;
638        try {
639            if (type == IMAGE) {
640                slideShowEditor.changeImage(slideNum, uri);
641            } else if (type == VIDEO) {
642                slideShowEditor.changeVideo(slideNum, uri);
643            } else if (type == AUDIO) {
644                slideShowEditor.changeAudio(slideNum, uri);
645            } else {
646                result = UNSUPPORTED_TYPE;
647            }
648        } catch (MmsException e) {
649            Log.e(TAG, "internalChangeMedia:", e);
650            result = UNKNOWN_ERROR;
651        } catch (UnsupportContentTypeException e) {
652            Log.e(TAG, "internalChangeMedia:", e);
653            result = UNSUPPORTED_TYPE;
654        } catch (ExceedMessageSizeException e) {
655            Log.e(TAG, "internalChangeMedia:", e);
656            result = MESSAGE_SIZE_EXCEEDED;
657        } catch (ResolutionException e) {
658            Log.e(TAG, "internalChangeMedia:", e);
659            result = IMAGE_TOO_LARGE;
660        }
661        return result;
662    }
663
664    /**
665     * Returns true if the message has an attachment (including slideshows).
666     */
667    public boolean hasAttachment() {
668        return (mAttachmentType > TEXT);
669    }
670
671    /**
672     * Returns the slideshow associated with this message.
673     */
674    public SlideshowModel getSlideshow() {
675        return mSlideshow;
676    }
677
678    /**
679     * Returns true if the message has a real slideshow, as opposed to just
680     * one image attachment, for example.
681     */
682    public boolean hasSlideshow() {
683        return (mAttachmentType == SLIDESHOW);
684    }
685
686    /**
687     * Sets the MMS subject of the message.  Passing null indicates that there
688     * is no subject.  Passing "" will result in an empty subject being added
689     * to the message, possibly triggering a conversion to MMS.  This extra
690     * bit of state is needed to support ComposeMessageActivity converting to
691     * MMS when the user adds a subject.  An empty subject will be removed
692     * before saving to disk or sending, however.
693     */
694    public void setSubject(CharSequence s, boolean notify) {
695        mSubject = s;
696        updateState(HAS_SUBJECT, (s != null), notify);
697    }
698
699    /**
700     * Returns the MMS subject of the message.
701     */
702    public CharSequence getSubject() {
703        return mSubject;
704    }
705
706    /**
707     * Returns true if this message has an MMS subject. A subject has to be more than just
708     * whitespace.
709     * @return
710     */
711    public boolean hasSubject() {
712        return mSubject != null && TextUtils.getTrimmedLength(mSubject) > 0;
713    }
714
715    /**
716     * Moves the message text into the slideshow.  Should be called any time
717     * the message is about to be sent or written to disk.
718     */
719    private void syncTextToSlideshow() {
720        if (mSlideshow == null || mSlideshow.size() != 1)
721            return;
722
723        SlideModel slide = mSlideshow.get(0);
724        TextModel text;
725        if (!slide.hasText()) {
726            // Add a TextModel to slide 0 if one doesn't already exist
727            text = new TextModel(mActivity, ContentType.TEXT_PLAIN, "text_0.txt",
728                                           mSlideshow.getLayout().getTextRegion());
729            slide.add(text);
730        } else {
731            // Otherwise just reuse the existing one.
732            text = slide.getText();
733        }
734        text.setText(mText);
735    }
736
737    /**
738     * Sets the message text out of the slideshow.  Should be called any time
739     * a slideshow is loaded from disk.
740     */
741    private void syncTextFromSlideshow() {
742        // Don't sync text for real slideshows.
743        if (mSlideshow.size() != 1) {
744            return;
745        }
746
747        SlideModel slide = mSlideshow.get(0);
748        if (slide == null || !slide.hasText()) {
749            return;
750        }
751
752        mText = slide.getText().getText();
753    }
754
755    /**
756     * Removes the subject if it is empty, possibly converting back to SMS.
757     */
758    private void removeSubjectIfEmpty(boolean notify) {
759        if (!hasSubject()) {
760            setSubject(null, notify);
761        }
762    }
763
764    /**
765     * Gets internal message state ready for storage.  Should be called any
766     * time the message is about to be sent or written to disk.
767     */
768    private void prepareForSave(boolean notify) {
769        // Make sure our working set of recipients is resolved
770        // to first-class Contact objects before we save.
771        syncWorkingRecipients();
772
773        if (hasMmsContentToSave()) {
774            ensureSlideshow();
775            syncTextToSlideshow();
776        }
777    }
778
779    /**
780     * Resolve the temporary working set of recipients to a ContactList.
781     */
782    public void syncWorkingRecipients() {
783        if (mWorkingRecipients != null) {
784            ContactList recipients = ContactList.getByNumbers(mWorkingRecipients, false);
785            mConversation.setRecipients(recipients);    // resets the threadId to zero
786            setHasMultipleRecipients(recipients.size() > 1, true);
787            mWorkingRecipients = null;
788        }
789    }
790
791    public String getWorkingRecipients() {
792        // this function is used for DEBUG only
793        if (mWorkingRecipients == null) {
794            return null;
795        }
796        ContactList recipients = ContactList.getByNumbers(mWorkingRecipients, false);
797        return recipients.serialize();
798    }
799
800    // Call when we've returned from adding an attachment. We're no longer forcing the message
801    // into a Mms message. At this point we either have the goods to make the message a Mms
802    // or we don't. No longer fake it.
803    public void removeFakeMmsForDraft() {
804        updateState(FORCE_MMS, false, false);
805    }
806
807    /**
808     * Force the message to be saved as MMS and return the Uri of the message.
809     * Typically used when handing a message off to another activity.
810     */
811    public Uri saveAsMms(boolean notify) {
812        if (DEBUG) LogTag.debug("saveAsMms mConversation=%s", mConversation);
813
814        // If we have discarded the message, just bail out.
815        if (mDiscarded) {
816            LogTag.warn("saveAsMms mDiscarded: true mConversation: " + mConversation +
817                    " returning NULL uri and bailing");
818            return null;
819        }
820
821        // FORCE_MMS behaves as sort of an "invisible attachment", making
822        // the message seem non-empty (and thus not discarded).  This bit
823        // is sticky until the last other MMS bit is removed, at which
824        // point the message will fall back to SMS.
825        updateState(FORCE_MMS, true, notify);
826
827        // Collect our state to be written to disk.
828        prepareForSave(true /* notify */);
829
830        try {
831            // Make sure we are saving to the correct thread ID.
832            DraftCache.getInstance().setSavingDraft(true);
833            if (!mConversation.getRecipients().isEmpty()) {
834                mConversation.ensureThreadId();
835            }
836            mConversation.setDraftState(true);
837
838            PduPersister persister = PduPersister.getPduPersister(mActivity);
839            SendReq sendReq = makeSendReq(mConversation, mSubject);
840
841            // If we don't already have a Uri lying around, make a new one.  If we do
842            // have one already, make sure it is synced to disk.
843            if (mMessageUri == null) {
844                mMessageUri = createDraftMmsMessage(persister, sendReq, mSlideshow, null,
845                        mActivity, null);
846            } else {
847                updateDraftMmsMessage(mMessageUri, persister, mSlideshow, sendReq, null);
848            }
849            mHasMmsDraft = true;
850        } finally {
851            DraftCache.getInstance().setSavingDraft(false);
852        }
853        return mMessageUri;
854    }
855
856    /**
857     * Save this message as a draft in the conversation previously specified
858     * to {@link setConversation}.
859     */
860    public void saveDraft(final boolean isStopping) {
861        // If we have discarded the message, just bail out.
862        if (mDiscarded) {
863            LogTag.warn("saveDraft mDiscarded: true mConversation: " + mConversation +
864                " skipping saving draft and bailing");
865            return;
866        }
867
868        // Make sure setConversation was called.
869        if (mConversation == null) {
870            throw new IllegalStateException("saveDraft() called with no conversation");
871        }
872
873        if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
874            LogTag.debug("saveDraft for mConversation " + mConversation);
875        }
876
877        // Get ready to write to disk. But don't notify message status when saving draft
878        prepareForSave(false /* notify */);
879
880        if (requiresMms()) {
881            if (hasMmsContentToSave()) {
882                asyncUpdateDraftMmsMessage(mConversation, isStopping);
883                mHasMmsDraft = true;
884            }
885        } else {
886            String content = mText.toString();
887
888            // bug 2169583: don't bother creating a thread id only to delete the thread
889            // because the content is empty. When we delete the thread in updateDraftSmsMessage,
890            // we didn't nullify conv.mThreadId, causing a temperary situation where conv
891            // is holding onto a thread id that isn't in the database. If a new message arrives
892            // and takes that thread id (because it's the next thread id to be assigned), the
893            // new message will be merged with the draft message thread, causing confusion!
894            if (!TextUtils.isEmpty(content)) {
895                asyncUpdateDraftSmsMessage(mConversation, content, isStopping);
896                mHasSmsDraft = true;
897            } else {
898                // When there's no associated text message, we have to handle the case where there
899                // might have been a previous mms draft for this message. This can happen when a
900                // user turns an mms back into a sms, such as creating an mms draft with a picture,
901                // then removing the picture.
902                asyncDeleteDraftMmsMessage(mConversation);
903                mMessageUri = null;
904            }
905        }
906    }
907
908    synchronized public void discard() {
909        if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
910            LogTag.debug("[WorkingMessage] discard");
911        }
912
913        if (mDiscarded == true) {
914            return;
915        }
916
917        // Mark this message as discarded in order to make saveDraft() no-op.
918        mDiscarded = true;
919
920        cancelThumbnailLoading();
921
922        // Delete any associated drafts if there are any.
923        if (mHasMmsDraft) {
924            asyncDeleteDraftMmsMessage(mConversation);
925        }
926        if (mHasSmsDraft) {
927            asyncDeleteDraftSmsMessage(mConversation);
928        }
929        clearConversation(mConversation, true);
930    }
931
932    public void unDiscard() {
933        if (DEBUG) LogTag.debug("unDiscard");
934
935        mDiscarded = false;
936    }
937
938    /**
939     * Returns true if discard() has been called on this message.
940     */
941    public boolean isDiscarded() {
942        return mDiscarded;
943    }
944
945    /**
946     * To be called from our Activity's onSaveInstanceState() to give us a chance
947     * to stow our state away for later retrieval.
948     *
949     * @param bundle The Bundle passed in to onSaveInstanceState
950     */
951    public void writeStateToBundle(Bundle bundle) {
952        if (hasSubject()) {
953            bundle.putString("subject", mSubject.toString());
954        }
955
956        if (mMessageUri != null) {
957            bundle.putParcelable("msg_uri", mMessageUri);
958        } else if (hasText()) {
959            bundle.putString("sms_body", mText.toString());
960        }
961    }
962
963    /**
964     * To be called from our Activity's onCreate() if the activity manager
965     * has given it a Bundle to reinflate
966     * @param bundle The Bundle passed in to onCreate
967     */
968    public void readStateFromBundle(Bundle bundle) {
969        if (bundle == null) {
970            return;
971        }
972
973        String subject = bundle.getString("subject");
974        setSubject(subject, false);
975
976        Uri uri = (Uri)bundle.getParcelable("msg_uri");
977        if (uri != null) {
978            loadFromUri(uri);
979            return;
980        } else {
981            String body = bundle.getString("sms_body");
982            mText = body;
983        }
984    }
985
986    /**
987     * Update the temporary list of recipients, used when setting up a
988     * new conversation.  Will be converted to a ContactList on any
989     * save event (send, save draft, etc.)
990     */
991    public void setWorkingRecipients(List<String> numbers) {
992        mWorkingRecipients = numbers;
993        String s = null;
994        if (numbers != null) {
995            int size = numbers.size();
996            switch (size) {
997            case 1:
998                s = numbers.get(0);
999                break;
1000            case 0:
1001                s = "empty";
1002                break;
1003            default:
1004                s = "{...} len=" + size;
1005            }
1006        }
1007    }
1008
1009    private void dumpWorkingRecipients() {
1010        Log.i(TAG, "-- mWorkingRecipients:");
1011
1012        if (mWorkingRecipients != null) {
1013            int count = mWorkingRecipients.size();
1014            for (int i=0; i<count; i++) {
1015                Log.i(TAG, "   [" + i + "] " + mWorkingRecipients.get(i));
1016            }
1017            Log.i(TAG, "");
1018        }
1019    }
1020
1021    public void dump() {
1022        Log.i(TAG, "WorkingMessage:");
1023        dumpWorkingRecipients();
1024        if (mConversation != null) {
1025            Log.i(TAG, "mConversation: " + mConversation.toString());
1026        }
1027    }
1028
1029    /**
1030     * Set the conversation associated with this message.
1031     */
1032    public void setConversation(Conversation conv) {
1033        if (DEBUG) LogTag.debug("setConversation %s -> %s", mConversation, conv);
1034
1035        mConversation = conv;
1036
1037        // Convert to MMS if there are any email addresses in the recipient list.
1038        ContactList contactList = conv.getRecipients();
1039        setHasEmail(contactList.containsEmail(), false);
1040        setHasMultipleRecipients(contactList.size() > 1, false);
1041    }
1042
1043    public Conversation getConversation() {
1044        return mConversation;
1045    }
1046
1047    /**
1048     * Hint whether or not this message will be delivered to an
1049     * an email address.
1050     */
1051    public void setHasEmail(boolean hasEmail, boolean notify) {
1052        if (MmsConfig.getEmailGateway() != null) {
1053            updateState(RECIPIENTS_REQUIRE_MMS, false, notify);
1054        } else {
1055            updateState(RECIPIENTS_REQUIRE_MMS, hasEmail, notify);
1056        }
1057    }
1058    /**
1059     * Set whether this message will be sent to multiple recipients. This is a hint whether the
1060     * message needs to be sent as an mms or not. If MmsConfig.getGroupMmsEnabled is false, then
1061     * the fact that the message is sent to multiple recipients is not a factor in determining
1062     * whether the message is sent as an mms, but the other factors (such as, "has a picture
1063     * attachment") still hold true.
1064     */
1065    public void setHasMultipleRecipients(boolean hasMultipleRecipients, boolean notify) {
1066        updateState(MULTIPLE_RECIPIENTS,
1067                hasMultipleRecipients &&
1068                    MessagingPreferenceActivity.getIsGroupMmsEnabled(mActivity),
1069                notify);
1070    }
1071
1072    /**
1073     * Returns true if this message would require MMS to send.
1074     */
1075    public boolean requiresMms() {
1076        return (mMmsState > 0);
1077    }
1078
1079    /**
1080     * Returns true if this message has been turned into an mms because it has a subject or
1081     * an attachment, but not just because it has multiple recipients.
1082     */
1083    private boolean hasMmsContentToSave() {
1084        if (mMmsState == 0) {
1085            return false;
1086        }
1087        if (mMmsState == MULTIPLE_RECIPIENTS && !hasText()) {
1088            // If this message is only mms because of multiple recipients and there's no text
1089            // to save, don't bother saving.
1090            return false;
1091        }
1092        return true;
1093    }
1094
1095    /**
1096     * Set whether or not we want to send this message via MMS in order to
1097     * avoid sending an excessive number of concatenated SMS messages.
1098     * @param: mmsRequired is the value for the LENGTH_REQUIRES_MMS bit.
1099     * @param: notify Whether or not to notify the user.
1100    */
1101    public void setLengthRequiresMms(boolean mmsRequired, boolean notify) {
1102        updateState(LENGTH_REQUIRES_MMS, mmsRequired, notify);
1103    }
1104
1105    private static String stateString(int state) {
1106        if (state == 0)
1107            return "<none>";
1108
1109        StringBuilder sb = new StringBuilder();
1110        if ((state & RECIPIENTS_REQUIRE_MMS) > 0)
1111            sb.append("RECIPIENTS_REQUIRE_MMS | ");
1112        if ((state & HAS_SUBJECT) > 0)
1113            sb.append("HAS_SUBJECT | ");
1114        if ((state & HAS_ATTACHMENT) > 0)
1115            sb.append("HAS_ATTACHMENT | ");
1116        if ((state & LENGTH_REQUIRES_MMS) > 0)
1117            sb.append("LENGTH_REQUIRES_MMS | ");
1118        if ((state & FORCE_MMS) > 0)
1119            sb.append("FORCE_MMS | ");
1120        if ((state & MULTIPLE_RECIPIENTS) > 0)
1121            sb.append("MULTIPLE_RECIPIENTS | ");
1122
1123        sb.delete(sb.length() - 3, sb.length());
1124        return sb.toString();
1125    }
1126
1127    /**
1128     * Sets the current state of our various "MMS required" bits.
1129     *
1130     * @param state The bit to change, such as {@link HAS_ATTACHMENT}
1131     * @param on If true, set it; if false, clear it
1132     * @param notify Whether or not to notify the user
1133     */
1134    private void updateState(int state, boolean on, boolean notify) {
1135        if (!sMmsEnabled) {
1136            // If Mms isn't enabled, the rest of the Messaging UI should not be using any
1137            // feature that would cause us to to turn on any Mms flag and show the
1138            // "Converting to multimedia..." message.
1139            return;
1140        }
1141        int oldState = mMmsState;
1142        if (on) {
1143            mMmsState |= state;
1144        } else {
1145            mMmsState &= ~state;
1146        }
1147
1148        // If we are clearing the last bit that is not FORCE_MMS,
1149        // expire the FORCE_MMS bit.
1150        if (mMmsState == FORCE_MMS && ((oldState & ~FORCE_MMS) > 0)) {
1151            mMmsState = 0;
1152        }
1153
1154        // Notify the listener if we are moving from SMS to MMS
1155        // or vice versa.
1156        if (notify) {
1157            if (oldState == 0 && mMmsState != 0) {
1158                mStatusListener.onProtocolChanged(true);
1159            } else if (oldState != 0 && mMmsState == 0) {
1160                mStatusListener.onProtocolChanged(false);
1161            }
1162        }
1163
1164        if (oldState != mMmsState) {
1165            if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) LogTag.debug("updateState: %s%s = %s",
1166                    on ? "+" : "-",
1167                    stateString(state), stateString(mMmsState));
1168        }
1169    }
1170
1171    /**
1172     * Send this message over the network.  Will call back with onMessageSent() once
1173     * it has been dispatched to the telephony stack.  This WorkingMessage object is
1174     * no longer useful after this method has been called.
1175     *
1176     * @throws ContentRestrictionException if sending an MMS and uaProfUrl is not defined
1177     * in mms_config.xml.
1178     */
1179    public void send(final String recipientsInUI) {
1180        long origThreadId = mConversation.getThreadId();
1181
1182        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
1183            LogTag.debug("send origThreadId: " + origThreadId);
1184        }
1185
1186        removeSubjectIfEmpty(true /* notify */);
1187
1188        // Get ready to write to disk.
1189        prepareForSave(true /* notify */);
1190
1191        // We need the recipient list for both SMS and MMS.
1192        final Conversation conv = mConversation;
1193        String msgTxt = mText.toString();
1194
1195        if (requiresMms() || addressContainsEmailToMms(conv, msgTxt)) {
1196            // uaProfUrl setting in mms_config.xml must be present to send an MMS.
1197            // However, SMS service will still work in the absence of a uaProfUrl address.
1198            if (MmsConfig.getUaProfUrl() == null) {
1199                String err = "WorkingMessage.send MMS sending failure. mms_config.xml is " +
1200                        "missing uaProfUrl setting.  uaProfUrl is required for MMS service, " +
1201                        "but can be absent for SMS.";
1202                RuntimeException ex = new NullPointerException(err);
1203                Log.e(TAG, err, ex);
1204                // now, let's just crash.
1205                throw ex;
1206            }
1207
1208            // Make local copies of the bits we need for sending a message,
1209            // because we will be doing it off of the main thread, which will
1210            // immediately continue on to resetting some of this state.
1211            final Uri mmsUri = mMessageUri;
1212            final PduPersister persister = PduPersister.getPduPersister(mActivity);
1213
1214            final SlideshowModel slideshow = mSlideshow;
1215            final CharSequence subject = mSubject;
1216            final boolean textOnly = mAttachmentType == TEXT;
1217
1218            if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
1219                LogTag.debug("Send mmsUri: " + mmsUri);
1220            }
1221
1222            // Do the dirty work of sending the message off of the main UI thread.
1223            new Thread(new Runnable() {
1224                @Override
1225                public void run() {
1226                    final SendReq sendReq = makeSendReq(conv, subject);
1227
1228                    // Make sure the text in slide 0 is no longer holding onto a reference to
1229                    // the text in the message text box.
1230                    slideshow.prepareForSend();
1231                    sendMmsWorker(conv, mmsUri, persister, slideshow, sendReq, textOnly);
1232
1233                    updateSendStats(conv);
1234                }
1235            }, "WorkingMessage.send MMS").start();
1236        } else {
1237            // Same rules apply as above.
1238            final String msgText = mText.toString();
1239            new Thread(new Runnable() {
1240                @Override
1241                public void run() {
1242                    preSendSmsWorker(conv, msgText, recipientsInUI);
1243
1244                    updateSendStats(conv);
1245                }
1246            }, "WorkingMessage.send SMS").start();
1247        }
1248
1249        // update the Recipient cache with the new to address, if it's different
1250        RecipientIdCache.updateNumbers(conv.getThreadId(), conv.getRecipients());
1251
1252        // Mark the message as discarded because it is "off the market" after being sent.
1253        mDiscarded = true;
1254    }
1255
1256    // Be sure to only call this on a background thread.
1257    private void updateSendStats(final Conversation conv) {
1258        String[] dests = conv.getRecipients().getNumbers();
1259        final ArrayList<String> phoneNumbers = new ArrayList<String>(Arrays.asList(dests));
1260
1261        DataUsageStatUpdater updater = new DataUsageStatUpdater(mActivity);
1262        updater.updateWithPhoneNumber(phoneNumbers);
1263    }
1264
1265    private boolean addressContainsEmailToMms(Conversation conv, String text) {
1266        if (MmsConfig.getEmailGateway() != null) {
1267            String[] dests = conv.getRecipients().getNumbers();
1268            int length = dests.length;
1269            for (int i = 0; i < length; i++) {
1270                if (Mms.isEmailAddress(dests[i]) || MessageUtils.isAlias(dests[i])) {
1271                    String mtext = dests[i] + " " + text;
1272                    int[] params = SmsMessage.calculateLength(mtext, false);
1273                    if (params[0] > 1) {
1274                        updateState(RECIPIENTS_REQUIRE_MMS, true, true);
1275                        ensureSlideshow();
1276                        syncTextToSlideshow();
1277                        return true;
1278                    }
1279                }
1280            }
1281        }
1282        return false;
1283    }
1284
1285    // Message sending stuff
1286
1287    private void preSendSmsWorker(Conversation conv, String msgText, String recipientsInUI) {
1288        // If user tries to send the message, it's a signal the inputted text is what they wanted.
1289        UserHappinessSignals.userAcceptedImeText(mActivity);
1290
1291        mStatusListener.onPreMessageSent();
1292
1293        long origThreadId = conv.getThreadId();
1294
1295        // Make sure we are still using the correct thread ID for our recipient set.
1296        long threadId = conv.ensureThreadId();
1297
1298        String semiSepRecipients = conv.getRecipients().serialize();
1299
1300        // recipientsInUI can be empty when the user types in a number and hits send
1301        if (LogTag.SEVERE_WARNING && ((origThreadId != 0 && origThreadId != threadId) ||
1302               (!semiSepRecipients.equals(recipientsInUI) && !TextUtils.isEmpty(recipientsInUI)))) {
1303            String msg = origThreadId != 0 && origThreadId != threadId ?
1304                    "WorkingMessage.preSendSmsWorker threadId changed or " +
1305                    "recipients changed. origThreadId: " +
1306                    origThreadId + " new threadId: " + threadId +
1307                    " also mConversation.getThreadId(): " +
1308                    mConversation.getThreadId()
1309                :
1310                    "Recipients in window: \"" +
1311                    recipientsInUI + "\" differ from recipients from conv: \"" +
1312                    semiSepRecipients + "\"";
1313
1314            LogTag.warnPossibleRecipientMismatch(msg, mActivity);
1315        }
1316
1317        // just do a regular send. We're already on a non-ui thread so no need to fire
1318        // off another thread to do this work.
1319        sendSmsWorker(msgText, semiSepRecipients, threadId);
1320
1321        // Be paranoid and clean any draft SMS up.
1322        deleteDraftSmsMessage(threadId);
1323    }
1324
1325    private void sendSmsWorker(String msgText, String semiSepRecipients, long threadId) {
1326        String[] dests = TextUtils.split(semiSepRecipients, ";");
1327        if (LogTag.VERBOSE || Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
1328            Log.d(LogTag.TRANSACTION, "sendSmsWorker sending message: recipients=" +
1329                    semiSepRecipients + ", threadId=" + threadId);
1330        }
1331        MessageSender sender = new SmsMessageSender(mActivity, dests, msgText, threadId);
1332        try {
1333            sender.sendMessage(threadId);
1334
1335            // Make sure this thread isn't over the limits in message count
1336            Recycler.getSmsRecycler().deleteOldMessagesByThreadId(mActivity, threadId);
1337        } catch (Exception e) {
1338            Log.e(TAG, "Failed to send SMS message, threadId=" + threadId, e);
1339        }
1340
1341        mStatusListener.onMessageSent();
1342        MmsWidgetProvider.notifyDatasetChanged(mActivity);
1343    }
1344
1345    private void sendMmsWorker(Conversation conv, Uri mmsUri, PduPersister persister,
1346            SlideshowModel slideshow, SendReq sendReq, boolean textOnly) {
1347        long threadId = 0;
1348        Cursor cursor = null;
1349        boolean newMessage = false;
1350        try {
1351            // Put a placeholder message in the database first
1352            DraftCache.getInstance().setSavingDraft(true);
1353            mStatusListener.onPreMessageSent();
1354
1355            // Make sure we are still using the correct thread ID for our
1356            // recipient set.
1357            threadId = conv.ensureThreadId();
1358
1359            if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1360                LogTag.debug("sendMmsWorker: update draft MMS message " + mmsUri +
1361                        " threadId: " + threadId);
1362            }
1363
1364            // One last check to verify the address of the recipient.
1365            String[] dests = conv.getRecipients().getNumbers(true /* scrub for MMS address */);
1366            if (dests.length == 1) {
1367                // verify the single address matches what's in the database. If we get a different
1368                // address back, jam the new value back into the SendReq.
1369                String newAddress =
1370                    Conversation.verifySingleRecipient(mActivity, conv.getThreadId(), dests[0]);
1371
1372                if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1373                    LogTag.debug("sendMmsWorker: newAddress " + newAddress +
1374                            " dests[0]: " + dests[0]);
1375                }
1376
1377                if (!newAddress.equals(dests[0])) {
1378                    dests[0] = newAddress;
1379                    EncodedStringValue[] encodedNumbers = EncodedStringValue.encodeStrings(dests);
1380                    if (encodedNumbers != null) {
1381                        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1382                            LogTag.debug("sendMmsWorker: REPLACING number!!!");
1383                        }
1384                        sendReq.setTo(encodedNumbers);
1385                    }
1386                }
1387            }
1388            newMessage = mmsUri == null;
1389            if (newMessage) {
1390                // Write something in the database so the new message will appear as sending
1391                ContentValues values = new ContentValues();
1392                values.put(Mms.MESSAGE_BOX, Mms.MESSAGE_BOX_OUTBOX);
1393                values.put(Mms.THREAD_ID, threadId);
1394                values.put(Mms.MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_SEND_REQ);
1395                if (textOnly) {
1396                    values.put(Mms.TEXT_ONLY, 1);
1397                }
1398                mmsUri = SqliteWrapper.insert(mActivity, mContentResolver, Mms.Outbox.CONTENT_URI,
1399                        values);
1400            }
1401            mStatusListener.onMessageSent();
1402
1403            // If user tries to send the message, it's a signal the inputted text is
1404            // what they wanted.
1405            UserHappinessSignals.userAcceptedImeText(mActivity);
1406
1407            // First make sure we don't have too many outstanding unsent message.
1408            cursor = SqliteWrapper.query(mActivity, mContentResolver,
1409                    Mms.Outbox.CONTENT_URI, MMS_OUTBOX_PROJECTION, null, null, null);
1410            if (cursor != null) {
1411                long maxMessageSize = MmsConfig.getMaxSizeScaleForPendingMmsAllowed() *
1412                MmsConfig.getMaxMessageSize();
1413                long totalPendingSize = 0;
1414                while (cursor.moveToNext()) {
1415                    totalPendingSize += cursor.getLong(MMS_MESSAGE_SIZE_INDEX);
1416                }
1417                if (totalPendingSize >= maxMessageSize) {
1418                    unDiscard();    // it wasn't successfully sent. Allow it to be saved as a draft.
1419                    mStatusListener.onMaxPendingMessagesReached();
1420                    markMmsMessageWithError(mmsUri);
1421                    return;
1422                }
1423            }
1424        } finally {
1425            if (cursor != null) {
1426                cursor.close();
1427            }
1428        }
1429
1430        try {
1431            if (newMessage) {
1432                // Create a new MMS message if one hasn't been made yet.
1433                mmsUri = createDraftMmsMessage(persister, sendReq, slideshow, mmsUri,
1434                        mActivity, null);
1435            } else {
1436                // Otherwise, sync the MMS message in progress to disk.
1437                updateDraftMmsMessage(mmsUri, persister, slideshow, sendReq, null);
1438            }
1439
1440            // Be paranoid and clean any draft SMS up.
1441            deleteDraftSmsMessage(threadId);
1442        } finally {
1443            DraftCache.getInstance().setSavingDraft(false);
1444        }
1445
1446        // Resize all the resizeable attachments (e.g. pictures) to fit
1447        // in the remaining space in the slideshow.
1448        int error = 0;
1449        try {
1450            slideshow.finalResize(mmsUri);
1451        } catch (ExceedMessageSizeException e1) {
1452            error = MESSAGE_SIZE_EXCEEDED;
1453        } catch (MmsException e1) {
1454            error = UNKNOWN_ERROR;
1455        }
1456        if (error != 0) {
1457            markMmsMessageWithError(mmsUri);
1458            mStatusListener.onAttachmentError(error);
1459            return;
1460        }
1461        MessageSender sender = new MmsMessageSender(mActivity, mmsUri,
1462                slideshow.getCurrentMessageSize());
1463        try {
1464            if (!sender.sendMessage(threadId)) {
1465                // The message was sent through SMS protocol, we should
1466                // delete the copy which was previously saved in MMS drafts.
1467                SqliteWrapper.delete(mActivity, mContentResolver, mmsUri, null, null);
1468            }
1469
1470            // Make sure this thread isn't over the limits in message count
1471            Recycler.getMmsRecycler().deleteOldMessagesByThreadId(mActivity, threadId);
1472        } catch (Exception e) {
1473            Log.e(TAG, "Failed to send message: " + mmsUri + ", threadId=" + threadId, e);
1474        }
1475        MmsWidgetProvider.notifyDatasetChanged(mActivity);
1476    }
1477
1478    private void markMmsMessageWithError(Uri mmsUri) {
1479        try {
1480            PduPersister p = PduPersister.getPduPersister(mActivity);
1481            // Move the message into MMS Outbox. A trigger will create an entry in
1482            // the "pending_msgs" table.
1483            p.move(mmsUri, Mms.Outbox.CONTENT_URI);
1484
1485            // Now update the pending_msgs table with an error for that new item.
1486            ContentValues values = new ContentValues(1);
1487            values.put(PendingMessages.ERROR_TYPE, MmsSms.ERR_TYPE_GENERIC_PERMANENT);
1488            long msgId = ContentUris.parseId(mmsUri);
1489            SqliteWrapper.update(mActivity, mContentResolver,
1490                    PendingMessages.CONTENT_URI,
1491                    values, PendingMessages.MSG_ID + "=" + msgId, null);
1492        } catch (MmsException e) {
1493            // Not much we can do here. If the p.move throws an exception, we'll just
1494            // leave the message in the draft box.
1495            Log.e(TAG, "Failed to move message to outbox and mark as error: " + mmsUri, e);
1496        }
1497    }
1498
1499    // Draft message stuff
1500
1501    private static final String[] MMS_DRAFT_PROJECTION = {
1502        Mms._ID,                // 0
1503        Mms.SUBJECT,            // 1
1504        Mms.SUBJECT_CHARSET     // 2
1505    };
1506
1507    private static final int MMS_ID_INDEX         = 0;
1508    private static final int MMS_SUBJECT_INDEX    = 1;
1509    private static final int MMS_SUBJECT_CS_INDEX = 2;
1510
1511    private static Uri readDraftMmsMessage(Context context, Conversation conv, StringBuilder sb) {
1512        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1513            LogTag.debug("readDraftMmsMessage conv: " + conv);
1514        }
1515        Cursor cursor;
1516        ContentResolver cr = context.getContentResolver();
1517
1518        final String selection = Mms.THREAD_ID + " = " + conv.getThreadId();
1519        cursor = SqliteWrapper.query(context, cr,
1520                Mms.Draft.CONTENT_URI, MMS_DRAFT_PROJECTION,
1521                selection, null, null);
1522
1523        Uri uri;
1524        try {
1525            if (cursor.moveToFirst()) {
1526                uri = ContentUris.withAppendedId(Mms.Draft.CONTENT_URI,
1527                        cursor.getLong(MMS_ID_INDEX));
1528                String subject = MessageUtils.extractEncStrFromCursor(cursor, MMS_SUBJECT_INDEX,
1529                        MMS_SUBJECT_CS_INDEX);
1530                if (subject != null) {
1531                    sb.append(subject);
1532                }
1533                if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1534                    LogTag.debug("readDraftMmsMessage uri: ", uri);
1535                }
1536                return uri;
1537            }
1538        } finally {
1539            cursor.close();
1540        }
1541
1542        return null;
1543    }
1544
1545    /**
1546     * makeSendReq should always return a non-null SendReq, whether the dest addresses are
1547     * valid or not.
1548     */
1549    private static SendReq makeSendReq(Conversation conv, CharSequence subject) {
1550        String[] dests = conv.getRecipients().getNumbers(true /* scrub for MMS address */);
1551
1552        SendReq req = new SendReq();
1553        EncodedStringValue[] encodedNumbers = EncodedStringValue.encodeStrings(dests);
1554        if (encodedNumbers != null) {
1555            req.setTo(encodedNumbers);
1556        }
1557
1558        if (!TextUtils.isEmpty(subject)) {
1559            req.setSubject(new EncodedStringValue(subject.toString()));
1560        }
1561
1562        req.setDate(System.currentTimeMillis() / 1000L);
1563
1564        return req;
1565    }
1566
1567    private static Uri createDraftMmsMessage(PduPersister persister, SendReq sendReq,
1568            SlideshowModel slideshow, Uri preUri, Context context,
1569            HashMap<Uri, InputStream> preOpenedFiles) {
1570        if (slideshow == null) {
1571            return null;
1572        }
1573        try {
1574            PduBody pb = slideshow.toPduBody();
1575            sendReq.setBody(pb);
1576            Uri res = persister.persist(sendReq, preUri == null ? Mms.Draft.CONTENT_URI : preUri,
1577                    true, MessagingPreferenceActivity.getIsGroupMmsEnabled(context),
1578                    preOpenedFiles);
1579            slideshow.sync(pb);
1580            return res;
1581        } catch (MmsException e) {
1582            return null;
1583        }
1584    }
1585
1586    private void asyncUpdateDraftMmsMessage(final Conversation conv, final boolean isStopping) {
1587        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1588            LogTag.debug("asyncUpdateDraftMmsMessage conv=%s mMessageUri=%s", conv, mMessageUri);
1589        }
1590        final HashMap<Uri, InputStream> preOpenedFiles =
1591                mSlideshow.openPartFiles(mContentResolver);
1592
1593        new Thread(new Runnable() {
1594            @Override
1595            public void run() {
1596                try {
1597                    DraftCache.getInstance().setSavingDraft(true);
1598
1599                    final PduPersister persister = PduPersister.getPduPersister(mActivity);
1600                    final SendReq sendReq = makeSendReq(conv, mSubject);
1601
1602                    if (mMessageUri == null) {
1603                        mMessageUri = createDraftMmsMessage(persister, sendReq, mSlideshow, null,
1604                                mActivity, preOpenedFiles);
1605                    } else {
1606                        updateDraftMmsMessage(mMessageUri, persister, mSlideshow, sendReq,
1607                                preOpenedFiles);
1608                    }
1609                    ensureThreadIdIfNeeded(conv, isStopping);
1610                    conv.setDraftState(true);
1611                    if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1612                        LogTag.debug("asyncUpdateDraftMmsMessage conv: " + conv +
1613                                " uri: " + mMessageUri);
1614                    }
1615
1616                    // Be paranoid and delete any SMS drafts that might be lying around. Must do
1617                    // this after ensureThreadId so conv has the correct thread id.
1618                    asyncDeleteDraftSmsMessage(conv);
1619                } finally {
1620                    DraftCache.getInstance().setSavingDraft(false);
1621                    closePreOpenedFiles(preOpenedFiles);
1622                }
1623            }
1624        }, "WorkingMessage.asyncUpdateDraftMmsMessage").start();
1625    }
1626
1627    private static void updateDraftMmsMessage(Uri uri, PduPersister persister,
1628            SlideshowModel slideshow, SendReq sendReq, HashMap<Uri, InputStream> preOpenedFiles) {
1629        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1630            LogTag.debug("updateDraftMmsMessage uri=%s", uri);
1631        }
1632        if (uri == null) {
1633            Log.e(TAG, "updateDraftMmsMessage null uri");
1634            return;
1635        }
1636        persister.updateHeaders(uri, sendReq);
1637
1638        final PduBody pb = slideshow.toPduBody();
1639
1640        try {
1641            persister.updateParts(uri, pb, preOpenedFiles);
1642        } catch (MmsException e) {
1643            Log.e(TAG, "updateDraftMmsMessage: cannot update message " + uri);
1644        }
1645
1646        slideshow.sync(pb);
1647    }
1648
1649    private static void closePreOpenedFiles(HashMap<Uri, InputStream> preOpenedFiles) {
1650        if (preOpenedFiles == null) {
1651            return;
1652        }
1653        Set<Uri> uris = preOpenedFiles.keySet();
1654        for (Uri uri : uris) {
1655            InputStream is = preOpenedFiles.get(uri);
1656            if (is != null) {
1657                try {
1658                    is.close();
1659                } catch (IOException e) {
1660                }
1661            }
1662        }
1663    }
1664
1665    private static final String SMS_DRAFT_WHERE = Sms.TYPE + "=" + Sms.MESSAGE_TYPE_DRAFT;
1666    private static final String[] SMS_BODY_PROJECTION = { Sms.BODY };
1667    private static final int SMS_BODY_INDEX = 0;
1668
1669    /**
1670     * Reads a draft message for the given thread ID from the database,
1671     * if there is one, deletes it from the database, and returns it.
1672     * @return The draft message or an empty string.
1673     */
1674    private String readDraftSmsMessage(Conversation conv) {
1675        long thread_id = conv.getThreadId();
1676        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1677            Log.d(TAG, "readDraftSmsMessage conv: " + conv);
1678        }
1679        // If it's an invalid thread or we know there's no draft, don't bother.
1680        if (thread_id <= 0 || !conv.hasDraft()) {
1681            return "";
1682        }
1683
1684        Uri thread_uri = ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, thread_id);
1685        String body = "";
1686
1687        Cursor c = SqliteWrapper.query(mActivity, mContentResolver,
1688                        thread_uri, SMS_BODY_PROJECTION, SMS_DRAFT_WHERE, null, null);
1689        boolean haveDraft = false;
1690        if (c != null) {
1691            try {
1692                if (c.moveToFirst()) {
1693                    body = c.getString(SMS_BODY_INDEX);
1694                    haveDraft = true;
1695                }
1696            } finally {
1697                c.close();
1698            }
1699        }
1700
1701        // We found a draft, and if there are no messages in the conversation,
1702        // that means we deleted the thread, too. Must reset the thread id
1703        // so we'll eventually create a new thread.
1704        if (haveDraft && conv.getMessageCount() == 0) {
1705            asyncDeleteDraftSmsMessage(conv);
1706
1707            // Clean out drafts for this thread -- if the recipient set changes,
1708            // we will lose track of the original draft and be unable to delete
1709            // it later.  The message will be re-saved if necessary upon exit of
1710            // the activity.
1711            clearConversation(conv, true);
1712        }
1713        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1714            LogTag.debug("readDraftSmsMessage haveDraft: ", !TextUtils.isEmpty(body));
1715        }
1716
1717        return body;
1718    }
1719
1720    public void clearConversation(final Conversation conv, boolean resetThreadId) {
1721        if (resetThreadId && conv.getMessageCount() == 0) {
1722            if (DEBUG) LogTag.debug("clearConversation calling clearThreadId");
1723            conv.clearThreadId();
1724        }
1725
1726        conv.setDraftState(false);
1727    }
1728
1729    private void asyncUpdateDraftSmsMessage(final Conversation conv, final String contents,
1730            final boolean isStopping) {
1731        new Thread(new Runnable() {
1732            @Override
1733            public void run() {
1734                try {
1735                    DraftCache.getInstance().setSavingDraft(true);
1736                    if (conv.getRecipients().isEmpty()) {
1737                        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1738                            LogTag.debug("asyncUpdateDraftSmsMessage no recipients, not saving");
1739                        }
1740                        return;
1741                    }
1742                    ensureThreadIdIfNeeded(conv, isStopping);
1743                    conv.setDraftState(true);
1744                    updateDraftSmsMessage(conv, contents);
1745                } finally {
1746                    DraftCache.getInstance().setSavingDraft(false);
1747                }
1748            }
1749        }, "WorkingMessage.asyncUpdateDraftSmsMessage").start();
1750    }
1751
1752    private void updateDraftSmsMessage(final Conversation conv, String contents) {
1753        final long threadId = conv.getThreadId();
1754        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1755            LogTag.debug("updateDraftSmsMessage tid=%d, contents=\"%s\"", threadId, contents);
1756        }
1757
1758        // If we don't have a valid thread, there's nothing to do.
1759        if (threadId <= 0) {
1760            return;
1761        }
1762
1763        ContentValues values = new ContentValues(3);
1764        values.put(Sms.THREAD_ID, threadId);
1765        values.put(Sms.BODY, contents);
1766        values.put(Sms.TYPE, Sms.MESSAGE_TYPE_DRAFT);
1767        SqliteWrapper.insert(mActivity, mContentResolver, Sms.CONTENT_URI, values);
1768        asyncDeleteDraftMmsMessage(conv);
1769        mMessageUri = null;
1770    }
1771
1772    private void asyncDelete(final Uri uri, final String selection, final String[] selectionArgs) {
1773        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1774            LogTag.debug("asyncDelete %s where %s", uri, selection);
1775        }
1776        new Thread(new Runnable() {
1777            @Override
1778            public void run() {
1779                SqliteWrapper.delete(mActivity, mContentResolver, uri, selection, selectionArgs);
1780            }
1781        }, "WorkingMessage.asyncDelete").start();
1782    }
1783
1784    public void asyncDeleteDraftSmsMessage(Conversation conv) {
1785        mHasSmsDraft = false;
1786
1787        final long threadId = conv.getThreadId();
1788        if (threadId > 0) {
1789            asyncDelete(ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, threadId),
1790                SMS_DRAFT_WHERE, null);
1791        }
1792    }
1793
1794    private void deleteDraftSmsMessage(long threadId) {
1795        SqliteWrapper.delete(mActivity, mContentResolver,
1796                ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, threadId),
1797                SMS_DRAFT_WHERE, null);
1798    }
1799
1800    private void asyncDeleteDraftMmsMessage(Conversation conv) {
1801        mHasMmsDraft = false;
1802
1803        final long threadId = conv.getThreadId();
1804        // If the thread id is < 1, then the thread_id in the pdu will be "" or NULL. We have
1805        // to clear those messages as well as ones with a valid thread id.
1806        final String where = Mms.THREAD_ID +  (threadId > 0 ? " = " + threadId : " IS NULL");
1807        asyncDelete(Mms.Draft.CONTENT_URI, where, null);
1808    }
1809
1810    /**
1811     * Ensure the thread id in conversation if needed, when we try to save a draft with a orphaned
1812     * one.
1813     * @param conv The conversation we are in.
1814     * @param isStopping Whether we are saving the draft in CMA'a onStop
1815     */
1816    private void ensureThreadIdIfNeeded(final Conversation conv, final boolean isStopping) {
1817        if (isStopping && conv.getMessageCount() == 0) {
1818            // We need to save the drafts in an unorphaned thread id. When the user goes
1819            // back to ConversationList while we're saving a draft from CMA's.onStop,
1820            // ConversationList will delete all threads from the thread table that
1821            // don't have associated sms or pdu entries. In case our thread got deleted,
1822            // well call clearThreadId() so ensureThreadId will query the db for the new
1823            // thread.
1824            conv.clearThreadId();   // force us to get the updated thread id
1825        }
1826        if (!conv.getRecipients().isEmpty()) {
1827            conv.ensureThreadId();
1828        }
1829    }
1830}
1831