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