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 = LogTag.TAG;
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(boolean showToast) {
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(), showToast);
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(false);
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(true);   // 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        }
498        return result;
499    }
500
501    /**
502     * Returns true if this message contains anything worth saving.
503     */
504    public boolean isWorthSaving() {
505        // If it actually contains anything, it's of course not empty.
506        if (hasText() || hasSubject() || hasAttachment() || hasSlideshow()) {
507            return true;
508        }
509
510        // When saveAsMms() has been called, we set FORCE_MMS to represent
511        // sort of an "invisible attachment" so that the message isn't thrown
512        // away when we are shipping it off to other activities.
513        if (isFakeMmsForDraft()) {
514            return true;
515        }
516
517        return false;
518    }
519
520    private void cancelThumbnailLoading() {
521        int numSlides = mSlideshow != null ? mSlideshow.size() : 0;
522        if (numSlides > 0) {
523            ImageModel imgModel = mSlideshow.get(numSlides - 1).getImage();
524            if (imgModel != null) {
525                imgModel.cancelThumbnailLoading();
526            }
527        }
528    }
529
530    /**
531     * Returns true if FORCE_MMS is set.
532     * When saveAsMms() has been called, we set FORCE_MMS to represent
533     * sort of an "invisible attachment" so that the message isn't thrown
534     * away when we are shipping it off to other activities.
535     */
536    public boolean isFakeMmsForDraft() {
537        return (mMmsState & FORCE_MMS) > 0;
538    }
539
540    /**
541     * Makes sure mSlideshow is set up.
542     */
543    private void ensureSlideshow() {
544        if (mSlideshow != null) {
545            return;
546        }
547
548        SlideshowModel slideshow = SlideshowModel.createNew(mActivity);
549        SlideModel slide = new SlideModel(slideshow);
550        slideshow.add(slide);
551
552        mSlideshow = slideshow;
553    }
554
555    /**
556     * Change the message's attachment to the data in the specified Uri.
557     * Used only for single-slide ("attachment mode") messages. If the attachment fails to
558     * attach, restore the slide to its original state.
559     */
560    private int changeMedia(int type, Uri uri, SlideshowEditor slideShowEditor) {
561        SlideModel originalSlide = mSlideshow.get(0);
562        if (originalSlide != null) {
563            slideShowEditor.removeSlide(0);     // remove the original slide
564        }
565        slideShowEditor.addNewSlide(0);
566        SlideModel slide = mSlideshow.get(0);   // get the new empty slide
567        int result = OK;
568
569        if (slide == null) {
570            Log.w(LogTag.TAG, "[WorkingMessage] changeMedia: no slides!");
571            return result;
572        }
573
574        // Clear the attachment type since we removed all the attachments. If this isn't cleared
575        // and the slide.add fails (for instance, a selected video could be too big), we'll be
576        // left in a state where we think we have an attachment, but it's been removed from the
577        // slide.
578        mAttachmentType = TEXT;
579
580        // If we're changing to text, just bail out.
581        if (type == TEXT) {
582            return result;
583        }
584
585        result = internalChangeMedia(type, uri, 0, slideShowEditor);
586        if (result != OK) {
587            slideShowEditor.removeSlide(0);             // remove the failed slide
588            if (originalSlide != null) {
589                slideShowEditor.addSlide(0, originalSlide); // restore the original slide.
590            }
591        }
592        return result;
593    }
594
595    /**
596     * Add the message's attachment to the data in the specified Uri to a new slide.
597     */
598    private int appendMedia(int type, Uri uri, SlideshowEditor slideShowEditor) {
599        int result = OK;
600
601        // If we're changing to text, just bail out.
602        if (type == TEXT) {
603            return result;
604        }
605
606        // The first time this method is called, mSlideshow.size() is going to be
607        // one (a newly initialized slideshow has one empty slide). The first time we
608        // attach the picture/video to that first empty slide. From then on when this
609        // function is called, we've got to create a new slide and add the picture/video
610        // to that new slide.
611        boolean addNewSlide = true;
612        if (mSlideshow.size() == 1 && !mSlideshow.isSimple()) {
613            addNewSlide = false;
614        }
615        if (addNewSlide) {
616            if (!slideShowEditor.addNewSlide()) {
617                return result;
618            }
619        }
620        int slideNum = mSlideshow.size() - 1;
621        result = internalChangeMedia(type, uri, slideNum, slideShowEditor);
622        if (result != OK) {
623            // We added a new slide and what we attempted to insert on the slide failed.
624            // Delete that slide, otherwise we could end up with a bunch of blank slides.
625            // It's ok that we're removing the slide even if we didn't add it (because it was
626            // the first default slide). If adding the first slide fails, we want to remove it.
627            slideShowEditor.removeSlide(slideNum);
628        }
629        return result;
630    }
631
632    private int internalChangeMedia(int type, Uri uri, int slideNum,
633            SlideshowEditor slideShowEditor) {
634        int result = OK;
635        try {
636            if (type == IMAGE) {
637                slideShowEditor.changeImage(slideNum, uri);
638            } else if (type == VIDEO) {
639                slideShowEditor.changeVideo(slideNum, uri);
640            } else if (type == AUDIO) {
641                slideShowEditor.changeAudio(slideNum, uri);
642            } else {
643                result = UNSUPPORTED_TYPE;
644            }
645        } catch (MmsException e) {
646            Log.e(TAG, "internalChangeMedia:", e);
647            result = UNKNOWN_ERROR;
648        } catch (UnsupportContentTypeException e) {
649            Log.e(TAG, "internalChangeMedia:", e);
650            result = UNSUPPORTED_TYPE;
651        } catch (ExceedMessageSizeException e) {
652            Log.e(TAG, "internalChangeMedia:", e);
653            result = MESSAGE_SIZE_EXCEEDED;
654        } catch (ResolutionException e) {
655            Log.e(TAG, "internalChangeMedia:", e);
656            result = IMAGE_TOO_LARGE;
657        }
658        return result;
659    }
660
661    /**
662     * Returns true if the message has an attachment (including slideshows).
663     */
664    public boolean hasAttachment() {
665        return (mAttachmentType > TEXT);
666    }
667
668    /**
669     * Returns the slideshow associated with this message.
670     */
671    public SlideshowModel getSlideshow() {
672        return mSlideshow;
673    }
674
675    /**
676     * Returns true if the message has a real slideshow, as opposed to just
677     * one image attachment, for example.
678     */
679    public boolean hasSlideshow() {
680        return (mAttachmentType == SLIDESHOW);
681    }
682
683    /**
684     * Sets the MMS subject of the message.  Passing null indicates that there
685     * is no subject.  Passing "" will result in an empty subject being added
686     * to the message, possibly triggering a conversion to MMS.  This extra
687     * bit of state is needed to support ComposeMessageActivity converting to
688     * MMS when the user adds a subject.  An empty subject will be removed
689     * before saving to disk or sending, however.
690     */
691    public void setSubject(CharSequence s, boolean notify) {
692        mSubject = s;
693        updateState(HAS_SUBJECT, (s != null), notify);
694    }
695
696    /**
697     * Returns the MMS subject of the message.
698     */
699    public CharSequence getSubject() {
700        return mSubject;
701    }
702
703    /**
704     * Returns true if this message has an MMS subject. A subject has to be more than just
705     * whitespace.
706     * @return
707     */
708    public boolean hasSubject() {
709        return mSubject != null && TextUtils.getTrimmedLength(mSubject) > 0;
710    }
711
712    /**
713     * Moves the message text into the slideshow.  Should be called any time
714     * the message is about to be sent or written to disk.
715     */
716    private void syncTextToSlideshow() {
717        if (mSlideshow == null || mSlideshow.size() != 1)
718            return;
719
720        SlideModel slide = mSlideshow.get(0);
721        TextModel text;
722        if (!slide.hasText()) {
723            // Add a TextModel to slide 0 if one doesn't already exist
724            text = new TextModel(mActivity, ContentType.TEXT_PLAIN, "text_0.txt",
725                                           mSlideshow.getLayout().getTextRegion());
726            slide.add(text);
727        } else {
728            // Otherwise just reuse the existing one.
729            text = slide.getText();
730        }
731        text.setText(mText);
732    }
733
734    /**
735     * Sets the message text out of the slideshow.  Should be called any time
736     * a slideshow is loaded from disk.
737     */
738    private void syncTextFromSlideshow() {
739        // Don't sync text for real slideshows.
740        if (mSlideshow.size() != 1) {
741            return;
742        }
743
744        SlideModel slide = mSlideshow.get(0);
745        if (slide == null || !slide.hasText()) {
746            return;
747        }
748
749        mText = slide.getText().getText();
750    }
751
752    /**
753     * Removes the subject if it is empty, possibly converting back to SMS.
754     */
755    private void removeSubjectIfEmpty(boolean notify) {
756        if (!hasSubject()) {
757            setSubject(null, notify);
758        }
759    }
760
761    /**
762     * Gets internal message state ready for storage.  Should be called any
763     * time the message is about to be sent or written to disk.
764     */
765    private void prepareForSave(boolean notify) {
766        // Make sure our working set of recipients is resolved
767        // to first-class Contact objects before we save.
768        syncWorkingRecipients();
769
770        if (hasMmsContentToSave()) {
771            ensureSlideshow();
772            syncTextToSlideshow();
773        }
774    }
775
776    /**
777     * Resolve the temporary working set of recipients to a ContactList.
778     */
779    public void syncWorkingRecipients() {
780        if (mWorkingRecipients != null) {
781            ContactList recipients = ContactList.getByNumbers(mWorkingRecipients, false);
782            mConversation.setRecipients(recipients);    // resets the threadId to zero
783            setHasMultipleRecipients(recipients.size() > 1, true);
784            mWorkingRecipients = null;
785        }
786    }
787
788    public String getWorkingRecipients() {
789        // this function is used for DEBUG only
790        if (mWorkingRecipients == null) {
791            return null;
792        }
793        ContactList recipients = ContactList.getByNumbers(mWorkingRecipients, false);
794        return recipients.serialize();
795    }
796
797    // Call when we've returned from adding an attachment. We're no longer forcing the message
798    // into a Mms message. At this point we either have the goods to make the message a Mms
799    // or we don't. No longer fake it.
800    public void removeFakeMmsForDraft() {
801        updateState(FORCE_MMS, false, false);
802    }
803
804    /**
805     * Force the message to be saved as MMS and return the Uri of the message.
806     * Typically used when handing a message off to another activity.
807     */
808    public Uri saveAsMms(boolean notify) {
809        if (DEBUG) LogTag.debug("saveAsMms mConversation=%s", mConversation);
810
811        // If we have discarded the message, just bail out.
812        if (mDiscarded) {
813            LogTag.warn("saveAsMms mDiscarded: true mConversation: " + mConversation +
814                    " returning NULL uri and bailing");
815            return null;
816        }
817
818        // FORCE_MMS behaves as sort of an "invisible attachment", making
819        // the message seem non-empty (and thus not discarded).  This bit
820        // is sticky until the last other MMS bit is removed, at which
821        // point the message will fall back to SMS.
822        updateState(FORCE_MMS, true, notify);
823
824        // Collect our state to be written to disk.
825        prepareForSave(true /* notify */);
826
827        try {
828            // Make sure we are saving to the correct thread ID.
829            DraftCache.getInstance().setSavingDraft(true);
830            if (!mConversation.getRecipients().isEmpty()) {
831                mConversation.ensureThreadId();
832            }
833            mConversation.setDraftState(true);
834
835            PduPersister persister = PduPersister.getPduPersister(mActivity);
836            SendReq sendReq = makeSendReq(mConversation, mSubject);
837
838            // If we don't already have a Uri lying around, make a new one.  If we do
839            // have one already, make sure it is synced to disk.
840            if (mMessageUri == null) {
841                mMessageUri = createDraftMmsMessage(persister, sendReq, mSlideshow, null,
842                        mActivity, null);
843            } else {
844                updateDraftMmsMessage(mMessageUri, persister, mSlideshow, sendReq, null);
845            }
846            mHasMmsDraft = true;
847        } finally {
848            DraftCache.getInstance().setSavingDraft(false);
849        }
850        return mMessageUri;
851    }
852
853    /**
854     * Save this message as a draft in the conversation previously specified
855     * to {@link setConversation}.
856     */
857    public void saveDraft(final boolean isStopping) {
858        // If we have discarded the message, just bail out.
859        if (mDiscarded) {
860            LogTag.warn("saveDraft mDiscarded: true mConversation: " + mConversation +
861                " skipping saving draft and bailing");
862            return;
863        }
864
865        // Make sure setConversation was called.
866        if (mConversation == null) {
867            throw new IllegalStateException("saveDraft() called with no conversation");
868        }
869
870        if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
871            LogTag.debug("saveDraft for mConversation " + mConversation);
872        }
873
874        // Get ready to write to disk. But don't notify message status when saving draft
875        prepareForSave(false /* notify */);
876
877        if (requiresMms()) {
878            if (hasMmsContentToSave()) {
879                asyncUpdateDraftMmsMessage(mConversation, isStopping);
880                mHasMmsDraft = true;
881            }
882        } else {
883            String content = mText.toString();
884
885            // bug 2169583: don't bother creating a thread id only to delete the thread
886            // because the content is empty. When we delete the thread in updateDraftSmsMessage,
887            // we didn't nullify conv.mThreadId, causing a temperary situation where conv
888            // is holding onto a thread id that isn't in the database. If a new message arrives
889            // and takes that thread id (because it's the next thread id to be assigned), the
890            // new message will be merged with the draft message thread, causing confusion!
891            if (!TextUtils.isEmpty(content)) {
892                asyncUpdateDraftSmsMessage(mConversation, content, isStopping);
893                mHasSmsDraft = true;
894            } else {
895                // When there's no associated text message, we have to handle the case where there
896                // might have been a previous mms draft for this message. This can happen when a
897                // user turns an mms back into a sms, such as creating an mms draft with a picture,
898                // then removing the picture.
899                asyncDeleteDraftMmsMessage(mConversation);
900                mMessageUri = null;
901            }
902        }
903    }
904
905    synchronized public void discard() {
906        if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
907            LogTag.debug("[WorkingMessage] discard");
908        }
909
910        if (mDiscarded == true) {
911            return;
912        }
913
914        // Mark this message as discarded in order to make saveDraft() no-op.
915        mDiscarded = true;
916
917        cancelThumbnailLoading();
918
919        // Delete any associated drafts if there are any.
920        if (mHasMmsDraft) {
921            asyncDeleteDraftMmsMessage(mConversation);
922        }
923        if (mHasSmsDraft) {
924            asyncDeleteDraftSmsMessage(mConversation);
925        }
926        clearConversation(mConversation, true);
927    }
928
929    public void unDiscard() {
930        if (DEBUG) LogTag.debug("unDiscard");
931
932        mDiscarded = false;
933    }
934
935    /**
936     * Returns true if discard() has been called on this message.
937     */
938    public boolean isDiscarded() {
939        return mDiscarded;
940    }
941
942    /**
943     * To be called from our Activity's onSaveInstanceState() to give us a chance
944     * to stow our state away for later retrieval.
945     *
946     * @param bundle The Bundle passed in to onSaveInstanceState
947     */
948    public void writeStateToBundle(Bundle bundle) {
949        if (hasSubject()) {
950            bundle.putString("subject", mSubject.toString());
951        }
952
953        if (mMessageUri != null) {
954            bundle.putParcelable("msg_uri", mMessageUri);
955        } else if (hasText()) {
956            bundle.putString("sms_body", mText.toString());
957        }
958    }
959
960    /**
961     * To be called from our Activity's onCreate() if the activity manager
962     * has given it a Bundle to reinflate
963     * @param bundle The Bundle passed in to onCreate
964     */
965    public void readStateFromBundle(Bundle bundle) {
966        if (bundle == null) {
967            return;
968        }
969
970        String subject = bundle.getString("subject");
971        setSubject(subject, false);
972
973        Uri uri = (Uri)bundle.getParcelable("msg_uri");
974        if (uri != null) {
975            loadFromUri(uri);
976            return;
977        } else {
978            String body = bundle.getString("sms_body");
979            mText = body;
980        }
981    }
982
983    /**
984     * Update the temporary list of recipients, used when setting up a
985     * new conversation.  Will be converted to a ContactList on any
986     * save event (send, save draft, etc.)
987     */
988    public void setWorkingRecipients(List<String> numbers) {
989        mWorkingRecipients = numbers;
990        String s = null;
991        if (numbers != null) {
992            int size = numbers.size();
993            switch (size) {
994            case 1:
995                s = numbers.get(0);
996                break;
997            case 0:
998                s = "empty";
999                break;
1000            default:
1001                s = "{...} len=" + size;
1002            }
1003        }
1004    }
1005
1006    private void dumpWorkingRecipients() {
1007        Log.i(TAG, "-- mWorkingRecipients:");
1008
1009        if (mWorkingRecipients != null) {
1010            int count = mWorkingRecipients.size();
1011            for (int i=0; i<count; i++) {
1012                Log.i(TAG, "   [" + i + "] " + mWorkingRecipients.get(i));
1013            }
1014            Log.i(TAG, "");
1015        }
1016    }
1017
1018    public void dump() {
1019        Log.i(TAG, "WorkingMessage:");
1020        dumpWorkingRecipients();
1021        if (mConversation != null) {
1022            Log.i(TAG, "mConversation: " + mConversation.toString());
1023        }
1024    }
1025
1026    /**
1027     * Set the conversation associated with this message.
1028     */
1029    public void setConversation(Conversation conv) {
1030        if (DEBUG) LogTag.debug("setConversation %s -> %s", mConversation, conv);
1031
1032        mConversation = conv;
1033
1034        // Convert to MMS if there are any email addresses in the recipient list.
1035        ContactList contactList = conv.getRecipients();
1036        setHasEmail(contactList.containsEmail(), false);
1037        setHasMultipleRecipients(contactList.size() > 1, false);
1038    }
1039
1040    public Conversation getConversation() {
1041        return mConversation;
1042    }
1043
1044    /**
1045     * Hint whether or not this message will be delivered to an
1046     * an email address.
1047     */
1048    public void setHasEmail(boolean hasEmail, boolean notify) {
1049        if (MmsConfig.getEmailGateway() != null) {
1050            updateState(RECIPIENTS_REQUIRE_MMS, false, notify);
1051        } else {
1052            updateState(RECIPIENTS_REQUIRE_MMS, hasEmail, notify);
1053        }
1054    }
1055    /**
1056     * Set whether this message will be sent to multiple recipients. This is a hint whether the
1057     * message needs to be sent as an mms or not. If MmsConfig.getGroupMmsEnabled is false, then
1058     * the fact that the message is sent to multiple recipients is not a factor in determining
1059     * whether the message is sent as an mms, but the other factors (such as, "has a picture
1060     * attachment") still hold true.
1061     */
1062    public void setHasMultipleRecipients(boolean hasMultipleRecipients, boolean notify) {
1063        updateState(MULTIPLE_RECIPIENTS,
1064                hasMultipleRecipients &&
1065                    MessagingPreferenceActivity.getIsGroupMmsEnabled(mActivity),
1066                notify);
1067    }
1068
1069    /**
1070     * Returns true if this message would require MMS to send.
1071     */
1072    public boolean requiresMms() {
1073        return (mMmsState > 0);
1074    }
1075
1076    /**
1077     * Returns true if this message has been turned into an mms because it has a subject or
1078     * an attachment, but not just because it has multiple recipients.
1079     */
1080    private boolean hasMmsContentToSave() {
1081        if (mMmsState == 0) {
1082            return false;
1083        }
1084        if (mMmsState == MULTIPLE_RECIPIENTS && !hasText()) {
1085            // If this message is only mms because of multiple recipients and there's no text
1086            // to save, don't bother saving.
1087            return false;
1088        }
1089        return true;
1090    }
1091
1092    /**
1093     * Set whether or not we want to send this message via MMS in order to
1094     * avoid sending an excessive number of concatenated SMS messages.
1095     * @param: mmsRequired is the value for the LENGTH_REQUIRES_MMS bit.
1096     * @param: notify Whether or not to notify the user.
1097    */
1098    public void setLengthRequiresMms(boolean mmsRequired, boolean notify) {
1099        updateState(LENGTH_REQUIRES_MMS, mmsRequired, notify);
1100    }
1101
1102    private static String stateString(int state) {
1103        if (state == 0)
1104            return "<none>";
1105
1106        StringBuilder sb = new StringBuilder();
1107        if ((state & RECIPIENTS_REQUIRE_MMS) > 0)
1108            sb.append("RECIPIENTS_REQUIRE_MMS | ");
1109        if ((state & HAS_SUBJECT) > 0)
1110            sb.append("HAS_SUBJECT | ");
1111        if ((state & HAS_ATTACHMENT) > 0)
1112            sb.append("HAS_ATTACHMENT | ");
1113        if ((state & LENGTH_REQUIRES_MMS) > 0)
1114            sb.append("LENGTH_REQUIRES_MMS | ");
1115        if ((state & FORCE_MMS) > 0)
1116            sb.append("FORCE_MMS | ");
1117        if ((state & MULTIPLE_RECIPIENTS) > 0)
1118            sb.append("MULTIPLE_RECIPIENTS | ");
1119
1120        sb.delete(sb.length() - 3, sb.length());
1121        return sb.toString();
1122    }
1123
1124    /**
1125     * Sets the current state of our various "MMS required" bits.
1126     *
1127     * @param state The bit to change, such as {@link HAS_ATTACHMENT}
1128     * @param on If true, set it; if false, clear it
1129     * @param notify Whether or not to notify the user
1130     */
1131    private void updateState(int state, boolean on, boolean notify) {
1132        if (!sMmsEnabled) {
1133            // If Mms isn't enabled, the rest of the Messaging UI should not be using any
1134            // feature that would cause us to to turn on any Mms flag and show the
1135            // "Converting to multimedia..." message.
1136            return;
1137        }
1138        int oldState = mMmsState;
1139        if (on) {
1140            mMmsState |= state;
1141        } else {
1142            mMmsState &= ~state;
1143        }
1144
1145        // If we are clearing the last bit that is not FORCE_MMS,
1146        // expire the FORCE_MMS bit.
1147        if (mMmsState == FORCE_MMS && ((oldState & ~FORCE_MMS) > 0)) {
1148            mMmsState = 0;
1149        }
1150
1151        // Notify the listener if we are moving from SMS to MMS
1152        // or vice versa.
1153        if (notify) {
1154            if (oldState == 0 && mMmsState != 0) {
1155                mStatusListener.onProtocolChanged(true);
1156            } else if (oldState != 0 && mMmsState == 0) {
1157                mStatusListener.onProtocolChanged(false);
1158            }
1159        }
1160
1161        if (oldState != mMmsState) {
1162            if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) LogTag.debug("updateState: %s%s = %s",
1163                    on ? "+" : "-",
1164                    stateString(state), stateString(mMmsState));
1165        }
1166    }
1167
1168    /**
1169     * Send this message over the network.  Will call back with onMessageSent() once
1170     * it has been dispatched to the telephony stack.  This WorkingMessage object is
1171     * no longer useful after this method has been called.
1172     *
1173     * @throws ContentRestrictionException if sending an MMS and uaProfUrl is not defined
1174     * in mms_config.xml.
1175     */
1176    public void send(final String recipientsInUI) {
1177        long origThreadId = mConversation.getThreadId();
1178
1179        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
1180            LogTag.debug("send origThreadId: " + origThreadId);
1181        }
1182
1183        removeSubjectIfEmpty(true /* notify */);
1184
1185        // Get ready to write to disk.
1186        prepareForSave(true /* notify */);
1187
1188        // We need the recipient list for both SMS and MMS.
1189        final Conversation conv = mConversation;
1190        String msgTxt = mText.toString();
1191
1192        if (requiresMms() || addressContainsEmailToMms(conv, msgTxt)) {
1193            // uaProfUrl setting in mms_config.xml must be present to send an MMS.
1194            // However, SMS service will still work in the absence of a uaProfUrl address.
1195            if (MmsConfig.getUaProfUrl() == null) {
1196                String err = "WorkingMessage.send MMS sending failure. mms_config.xml is " +
1197                        "missing uaProfUrl setting.  uaProfUrl is required for MMS service, " +
1198                        "but can be absent for SMS.";
1199                RuntimeException ex = new NullPointerException(err);
1200                Log.e(TAG, err, ex);
1201                // now, let's just crash.
1202                throw ex;
1203            }
1204
1205            // Make local copies of the bits we need for sending a message,
1206            // because we will be doing it off of the main thread, which will
1207            // immediately continue on to resetting some of this state.
1208            final Uri mmsUri = mMessageUri;
1209            final PduPersister persister = PduPersister.getPduPersister(mActivity);
1210
1211            final SlideshowModel slideshow = mSlideshow;
1212            final CharSequence subject = mSubject;
1213            final boolean textOnly = mAttachmentType == TEXT;
1214
1215            if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
1216                LogTag.debug("Send mmsUri: " + mmsUri);
1217            }
1218
1219            // Do the dirty work of sending the message off of the main UI thread.
1220            new Thread(new Runnable() {
1221                @Override
1222                public void run() {
1223                    final SendReq sendReq = makeSendReq(conv, subject);
1224
1225                    // Make sure the text in slide 0 is no longer holding onto a reference to
1226                    // the text in the message text box.
1227                    slideshow.prepareForSend();
1228                    sendMmsWorker(conv, mmsUri, persister, slideshow, sendReq, textOnly);
1229
1230                    updateSendStats(conv);
1231                }
1232            }, "WorkingMessage.send MMS").start();
1233        } else {
1234            // Same rules apply as above.
1235            final String msgText = mText.toString();
1236            new Thread(new Runnable() {
1237                @Override
1238                public void run() {
1239                    preSendSmsWorker(conv, msgText, recipientsInUI);
1240
1241                    updateSendStats(conv);
1242                }
1243            }, "WorkingMessage.send SMS").start();
1244        }
1245
1246        // update the Recipient cache with the new to address, if it's different
1247        RecipientIdCache.updateNumbers(conv.getThreadId(), conv.getRecipients());
1248
1249        // Mark the message as discarded because it is "off the market" after being sent.
1250        mDiscarded = true;
1251    }
1252
1253    // Be sure to only call this on a background thread.
1254    private void updateSendStats(final Conversation conv) {
1255        String[] dests = conv.getRecipients().getNumbers();
1256        final ArrayList<String> phoneNumbers = new ArrayList<String>(Arrays.asList(dests));
1257
1258        DataUsageStatUpdater updater = new DataUsageStatUpdater(mActivity);
1259        updater.updateWithPhoneNumber(phoneNumbers);
1260    }
1261
1262    private boolean addressContainsEmailToMms(Conversation conv, String text) {
1263        if (MmsConfig.getEmailGateway() != null) {
1264            String[] dests = conv.getRecipients().getNumbers();
1265            int length = dests.length;
1266            for (int i = 0; i < length; i++) {
1267                if (Mms.isEmailAddress(dests[i]) || MessageUtils.isAlias(dests[i])) {
1268                    String mtext = dests[i] + " " + text;
1269                    int[] params = SmsMessage.calculateLength(mtext, false);
1270                    if (params[0] > 1) {
1271                        updateState(RECIPIENTS_REQUIRE_MMS, true, true);
1272                        ensureSlideshow();
1273                        syncTextToSlideshow();
1274                        return true;
1275                    }
1276                }
1277            }
1278        }
1279        return false;
1280    }
1281
1282    // Message sending stuff
1283
1284    private void preSendSmsWorker(Conversation conv, String msgText, String recipientsInUI) {
1285        // If user tries to send the message, it's a signal the inputted text is what they wanted.
1286        UserHappinessSignals.userAcceptedImeText(mActivity);
1287
1288        mStatusListener.onPreMessageSent();
1289
1290        long origThreadId = conv.getThreadId();
1291
1292        // Make sure we are still using the correct thread ID for our recipient set.
1293        long threadId = conv.ensureThreadId();
1294
1295        String semiSepRecipients = conv.getRecipients().serialize();
1296
1297        // recipientsInUI can be empty when the user types in a number and hits send
1298        if (LogTag.SEVERE_WARNING && ((origThreadId != 0 && origThreadId != threadId) ||
1299               (!semiSepRecipients.equals(recipientsInUI) && !TextUtils.isEmpty(recipientsInUI)))) {
1300            String msg = origThreadId != 0 && origThreadId != threadId ?
1301                    "WorkingMessage.preSendSmsWorker threadId changed or " +
1302                    "recipients changed. origThreadId: " +
1303                    origThreadId + " new threadId: " + threadId +
1304                    " also mConversation.getThreadId(): " +
1305                    mConversation.getThreadId()
1306                :
1307                    "Recipients in window: \"" +
1308                    recipientsInUI + "\" differ from recipients from conv: \"" +
1309                    semiSepRecipients + "\"";
1310
1311            // Just interrupt the process of sending message if recipient mismatch
1312            LogTag.warnPossibleRecipientMismatch(msg, mActivity);
1313        }else {
1314            // just do a regular send. We're already on a non-ui thread so no need to fire
1315            // off another thread to do this work.
1316            sendSmsWorker(msgText, semiSepRecipients, threadId);
1317
1318            // Be paranoid and clean any draft SMS up.
1319            deleteDraftSmsMessage(threadId);
1320        }
1321    }
1322
1323    private void sendSmsWorker(String msgText, String semiSepRecipients, long threadId) {
1324        String[] dests = TextUtils.split(semiSepRecipients, ";");
1325        if (LogTag.VERBOSE || Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
1326            Log.d(LogTag.TRANSACTION, "sendSmsWorker sending message: recipients=" +
1327                    semiSepRecipients + ", threadId=" + threadId);
1328        }
1329        MessageSender sender = new SmsMessageSender(mActivity, dests, msgText, threadId);
1330        try {
1331            sender.sendMessage(threadId);
1332
1333            // Make sure this thread isn't over the limits in message count
1334            Recycler.getSmsRecycler().deleteOldMessagesByThreadId(mActivity, threadId);
1335        } catch (Exception e) {
1336            Log.e(TAG, "Failed to send SMS message, threadId=" + threadId, e);
1337        }
1338
1339        mStatusListener.onMessageSent();
1340        MmsWidgetProvider.notifyDatasetChanged(mActivity);
1341    }
1342
1343    private void sendMmsWorker(Conversation conv, Uri mmsUri, PduPersister persister,
1344            SlideshowModel slideshow, SendReq sendReq, boolean textOnly) {
1345        long threadId = 0;
1346        Cursor cursor = null;
1347        boolean newMessage = false;
1348        try {
1349            // Put a placeholder message in the database first
1350            DraftCache.getInstance().setSavingDraft(true);
1351            mStatusListener.onPreMessageSent();
1352
1353            // Make sure we are still using the correct thread ID for our
1354            // recipient set.
1355            threadId = conv.ensureThreadId();
1356
1357            if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1358                LogTag.debug("sendMmsWorker: update draft MMS message " + mmsUri +
1359                        " threadId: " + threadId);
1360            }
1361
1362            // One last check to verify the address of the recipient.
1363            String[] dests = conv.getRecipients().getNumbers(true /* scrub for MMS address */);
1364            if (dests.length == 1) {
1365                // verify the single address matches what's in the database. If we get a different
1366                // address back, jam the new value back into the SendReq.
1367                String newAddress =
1368                    Conversation.verifySingleRecipient(mActivity, conv.getThreadId(), dests[0]);
1369
1370                if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1371                    LogTag.debug("sendMmsWorker: newAddress " + newAddress +
1372                            " dests[0]: " + dests[0]);
1373                }
1374
1375                if (!newAddress.equals(dests[0])) {
1376                    dests[0] = newAddress;
1377                    EncodedStringValue[] encodedNumbers = EncodedStringValue.encodeStrings(dests);
1378                    if (encodedNumbers != null) {
1379                        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1380                            LogTag.debug("sendMmsWorker: REPLACING number!!!");
1381                        }
1382                        sendReq.setTo(encodedNumbers);
1383                    }
1384                }
1385            }
1386            newMessage = mmsUri == null;
1387            if (newMessage) {
1388                // Write something in the database so the new message will appear as sending
1389                ContentValues values = new ContentValues();
1390                values.put(Mms.MESSAGE_BOX, Mms.MESSAGE_BOX_OUTBOX);
1391                values.put(Mms.THREAD_ID, threadId);
1392                values.put(Mms.MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_SEND_REQ);
1393                if (textOnly) {
1394                    values.put(Mms.TEXT_ONLY, 1);
1395                }
1396                mmsUri = SqliteWrapper.insert(mActivity, mContentResolver, Mms.Outbox.CONTENT_URI,
1397                        values);
1398            }
1399            mStatusListener.onMessageSent();
1400
1401            // If user tries to send the message, it's a signal the inputted text is
1402            // what they wanted.
1403            UserHappinessSignals.userAcceptedImeText(mActivity);
1404
1405            // First make sure we don't have too many outstanding unsent message.
1406            cursor = SqliteWrapper.query(mActivity, mContentResolver,
1407                    Mms.Outbox.CONTENT_URI, MMS_OUTBOX_PROJECTION, null, null, null);
1408            if (cursor != null) {
1409                long maxMessageSize = MmsConfig.getMaxSizeScaleForPendingMmsAllowed() *
1410                MmsConfig.getMaxMessageSize();
1411                long totalPendingSize = 0;
1412                while (cursor.moveToNext()) {
1413                    totalPendingSize += cursor.getLong(MMS_MESSAGE_SIZE_INDEX);
1414                }
1415                if (totalPendingSize >= maxMessageSize) {
1416                    unDiscard();    // it wasn't successfully sent. Allow it to be saved as a draft.
1417                    mStatusListener.onMaxPendingMessagesReached();
1418                    markMmsMessageWithError(mmsUri);
1419                    return;
1420                }
1421            }
1422        } finally {
1423            if (cursor != null) {
1424                cursor.close();
1425            }
1426        }
1427
1428        try {
1429            if (newMessage) {
1430                // Create a new MMS message if one hasn't been made yet.
1431                mmsUri = createDraftMmsMessage(persister, sendReq, slideshow, mmsUri,
1432                        mActivity, null);
1433            } else {
1434                // Otherwise, sync the MMS message in progress to disk.
1435                updateDraftMmsMessage(mmsUri, persister, slideshow, sendReq, null);
1436            }
1437
1438            // Be paranoid and clean any draft SMS up.
1439            deleteDraftSmsMessage(threadId);
1440        } finally {
1441            DraftCache.getInstance().setSavingDraft(false);
1442        }
1443
1444        // Resize all the resizeable attachments (e.g. pictures) to fit
1445        // in the remaining space in the slideshow.
1446        int error = 0;
1447        try {
1448            slideshow.finalResize(mmsUri);
1449        } catch (ExceedMessageSizeException e1) {
1450            error = MESSAGE_SIZE_EXCEEDED;
1451        } catch (MmsException e1) {
1452            error = UNKNOWN_ERROR;
1453        }
1454        if (error != 0) {
1455            markMmsMessageWithError(mmsUri);
1456            mStatusListener.onAttachmentError(error);
1457            return;
1458        }
1459        MessageSender sender = new MmsMessageSender(mActivity, mmsUri,
1460                slideshow.getCurrentMessageSize());
1461        try {
1462            if (!sender.sendMessage(threadId)) {
1463                // The message was sent through SMS protocol, we should
1464                // delete the copy which was previously saved in MMS drafts.
1465                SqliteWrapper.delete(mActivity, mContentResolver, mmsUri, null, null);
1466            }
1467
1468            // Make sure this thread isn't over the limits in message count
1469            Recycler.getMmsRecycler().deleteOldMessagesByThreadId(mActivity, threadId);
1470        } catch (Exception e) {
1471            Log.e(TAG, "Failed to send message: " + mmsUri + ", threadId=" + threadId, e);
1472        }
1473        MmsWidgetProvider.notifyDatasetChanged(mActivity);
1474    }
1475
1476    private void markMmsMessageWithError(Uri mmsUri) {
1477        try {
1478            PduPersister p = PduPersister.getPduPersister(mActivity);
1479            // Move the message into MMS Outbox. A trigger will create an entry in
1480            // the "pending_msgs" table.
1481            p.move(mmsUri, Mms.Outbox.CONTENT_URI);
1482
1483            // Now update the pending_msgs table with an error for that new item.
1484            ContentValues values = new ContentValues(1);
1485            values.put(PendingMessages.ERROR_TYPE, MmsSms.ERR_TYPE_GENERIC_PERMANENT);
1486            long msgId = ContentUris.parseId(mmsUri);
1487            SqliteWrapper.update(mActivity, mContentResolver,
1488                    PendingMessages.CONTENT_URI,
1489                    values, PendingMessages.MSG_ID + "=" + msgId, null);
1490        } catch (MmsException e) {
1491            // Not much we can do here. If the p.move throws an exception, we'll just
1492            // leave the message in the draft box.
1493            Log.e(TAG, "Failed to move message to outbox and mark as error: " + mmsUri, e);
1494        }
1495    }
1496
1497    // Draft message stuff
1498
1499    private static final String[] MMS_DRAFT_PROJECTION = {
1500        Mms._ID,                // 0
1501        Mms.SUBJECT,            // 1
1502        Mms.SUBJECT_CHARSET     // 2
1503    };
1504
1505    private static final int MMS_ID_INDEX         = 0;
1506    private static final int MMS_SUBJECT_INDEX    = 1;
1507    private static final int MMS_SUBJECT_CS_INDEX = 2;
1508
1509    private static Uri readDraftMmsMessage(Context context, Conversation conv, StringBuilder sb) {
1510        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1511            LogTag.debug("readDraftMmsMessage conv: " + conv);
1512        }
1513        Cursor cursor;
1514        ContentResolver cr = context.getContentResolver();
1515
1516        final String selection = Mms.THREAD_ID + " = " + conv.getThreadId();
1517        cursor = SqliteWrapper.query(context, cr,
1518                Mms.Draft.CONTENT_URI, MMS_DRAFT_PROJECTION,
1519                selection, null, null);
1520        if (cursor == null) {
1521            return null;
1522        }
1523
1524        Uri uri;
1525        try {
1526            if (cursor.moveToFirst()) {
1527                uri = ContentUris.withAppendedId(Mms.Draft.CONTENT_URI,
1528                        cursor.getLong(MMS_ID_INDEX));
1529                String subject = MessageUtils.extractEncStrFromCursor(cursor, MMS_SUBJECT_INDEX,
1530                        MMS_SUBJECT_CS_INDEX);
1531                if (subject != null) {
1532                    sb.append(subject);
1533                }
1534                if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1535                    LogTag.debug("readDraftMmsMessage uri: ", uri);
1536                }
1537                return uri;
1538            }
1539        } finally {
1540            cursor.close();
1541        }
1542
1543        return null;
1544    }
1545
1546    /**
1547     * makeSendReq should always return a non-null SendReq, whether the dest addresses are
1548     * valid or not.
1549     */
1550    private static SendReq makeSendReq(Conversation conv, CharSequence subject) {
1551        String[] dests = conv.getRecipients().getNumbers(true /* scrub for MMS address */);
1552
1553        SendReq req = new SendReq();
1554        EncodedStringValue[] encodedNumbers = EncodedStringValue.encodeStrings(dests);
1555        if (encodedNumbers != null) {
1556            req.setTo(encodedNumbers);
1557        }
1558
1559        if (!TextUtils.isEmpty(subject)) {
1560            req.setSubject(new EncodedStringValue(subject.toString()));
1561        }
1562
1563        req.setDate(System.currentTimeMillis() / 1000L);
1564
1565        return req;
1566    }
1567
1568    private static Uri createDraftMmsMessage(PduPersister persister, SendReq sendReq,
1569            SlideshowModel slideshow, Uri preUri, Context context,
1570            HashMap<Uri, InputStream> preOpenedFiles) {
1571        if (slideshow == null) {
1572            return null;
1573        }
1574        try {
1575            PduBody pb = slideshow.toPduBody();
1576            sendReq.setBody(pb);
1577            Uri res = persister.persist(sendReq, preUri == null ? Mms.Draft.CONTENT_URI : preUri,
1578                    true, MessagingPreferenceActivity.getIsGroupMmsEnabled(context),
1579                    preOpenedFiles);
1580            slideshow.sync(pb);
1581            return res;
1582        } catch (MmsException e) {
1583            return null;
1584        } catch (IllegalStateException e) {
1585            Log.e(TAG,"failed to create draft mms "+ e);
1586            return null;
1587        }
1588    }
1589
1590    private void asyncUpdateDraftMmsMessage(final Conversation conv, final boolean isStopping) {
1591        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1592            LogTag.debug("asyncUpdateDraftMmsMessage conv=%s mMessageUri=%s", conv, mMessageUri);
1593        }
1594        final HashMap<Uri, InputStream> preOpenedFiles =
1595                mSlideshow.openPartFiles(mContentResolver);
1596
1597        new Thread(new Runnable() {
1598            @Override
1599            public void run() {
1600                try {
1601                    DraftCache.getInstance().setSavingDraft(true);
1602
1603                    final PduPersister persister = PduPersister.getPduPersister(mActivity);
1604                    final SendReq sendReq = makeSendReq(conv, mSubject);
1605
1606                    if (mMessageUri == null) {
1607                        mMessageUri = createDraftMmsMessage(persister, sendReq, mSlideshow, null,
1608                                mActivity, preOpenedFiles);
1609                    } else {
1610                        updateDraftMmsMessage(mMessageUri, persister, mSlideshow, sendReq,
1611                                preOpenedFiles);
1612                    }
1613                    ensureThreadIdIfNeeded(conv, isStopping);
1614                    conv.setDraftState(true);
1615                    if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1616                        LogTag.debug("asyncUpdateDraftMmsMessage conv: " + conv +
1617                                " uri: " + mMessageUri);
1618                    }
1619
1620                    // Be paranoid and delete any SMS drafts that might be lying around. Must do
1621                    // this after ensureThreadId so conv has the correct thread id.
1622                    asyncDeleteDraftSmsMessage(conv);
1623                } finally {
1624                    DraftCache.getInstance().setSavingDraft(false);
1625                    closePreOpenedFiles(preOpenedFiles);
1626                }
1627            }
1628        }, "WorkingMessage.asyncUpdateDraftMmsMessage").start();
1629    }
1630
1631    private static void updateDraftMmsMessage(Uri uri, PduPersister persister,
1632            SlideshowModel slideshow, SendReq sendReq, HashMap<Uri, InputStream> preOpenedFiles) {
1633        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1634            LogTag.debug("updateDraftMmsMessage uri=%s", uri);
1635        }
1636        if (uri == null) {
1637            Log.e(TAG, "updateDraftMmsMessage null uri");
1638            return;
1639        }
1640        persister.updateHeaders(uri, sendReq);
1641
1642        final PduBody pb = slideshow.toPduBody();
1643
1644        try {
1645            persister.updateParts(uri, pb, preOpenedFiles);
1646        } catch (MmsException e) {
1647            Log.e(TAG, "updateDraftMmsMessage: cannot update message " + uri);
1648        }
1649
1650        slideshow.sync(pb);
1651    }
1652
1653    private static void closePreOpenedFiles(HashMap<Uri, InputStream> preOpenedFiles) {
1654        if (preOpenedFiles == null) {
1655            return;
1656        }
1657        Set<Uri> uris = preOpenedFiles.keySet();
1658        for (Uri uri : uris) {
1659            InputStream is = preOpenedFiles.get(uri);
1660            if (is != null) {
1661                try {
1662                    is.close();
1663                } catch (IOException e) {
1664                }
1665            }
1666        }
1667    }
1668
1669    private static final String SMS_DRAFT_WHERE = Sms.TYPE + "=" + Sms.MESSAGE_TYPE_DRAFT;
1670    private static final String[] SMS_BODY_PROJECTION = { Sms.BODY };
1671    private static final int SMS_BODY_INDEX = 0;
1672
1673    /**
1674     * Reads a draft message for the given thread ID from the database,
1675     * if there is one, deletes it from the database, and returns it.
1676     * @return The draft message or an empty string.
1677     */
1678    private String readDraftSmsMessage(Conversation conv) {
1679        long thread_id = conv.getThreadId();
1680        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1681            Log.d(TAG, "readDraftSmsMessage conv: " + conv);
1682        }
1683        // If it's an invalid thread or we know there's no draft, don't bother.
1684        if (thread_id <= 0 || !conv.hasDraft()) {
1685            return "";
1686        }
1687
1688        Uri thread_uri = ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, thread_id);
1689        String body = "";
1690
1691        Cursor c = SqliteWrapper.query(mActivity, mContentResolver,
1692                        thread_uri, SMS_BODY_PROJECTION, SMS_DRAFT_WHERE, null, null);
1693        boolean haveDraft = false;
1694        if (c != null) {
1695            try {
1696                if (c.moveToFirst()) {
1697                    body = c.getString(SMS_BODY_INDEX);
1698                    haveDraft = true;
1699                }
1700            } finally {
1701                c.close();
1702            }
1703        }
1704
1705        // We found a draft, and if there are no messages in the conversation,
1706        // that means we deleted the thread, too. Must reset the thread id
1707        // so we'll eventually create a new thread.
1708        if (haveDraft && conv.getMessageCount() == 0) {
1709            asyncDeleteDraftSmsMessage(conv);
1710
1711            // Clean out drafts for this thread -- if the recipient set changes,
1712            // we will lose track of the original draft and be unable to delete
1713            // it later.  The message will be re-saved if necessary upon exit of
1714            // the activity.
1715            clearConversation(conv, true);
1716        }
1717        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1718            LogTag.debug("readDraftSmsMessage haveDraft: ", !TextUtils.isEmpty(body));
1719        }
1720
1721        return body;
1722    }
1723
1724    public void clearConversation(final Conversation conv, boolean resetThreadId) {
1725        if (resetThreadId && conv.getMessageCount() == 0) {
1726            if (DEBUG) LogTag.debug("clearConversation calling clearThreadId");
1727            conv.clearThreadId();
1728        }
1729
1730        conv.setDraftState(false);
1731    }
1732
1733    private void asyncUpdateDraftSmsMessage(final Conversation conv, final String contents,
1734            final boolean isStopping) {
1735        new Thread(new Runnable() {
1736            @Override
1737            public void run() {
1738                try {
1739                    DraftCache.getInstance().setSavingDraft(true);
1740                    if (conv.getRecipients().isEmpty()) {
1741                        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1742                            LogTag.debug("asyncUpdateDraftSmsMessage no recipients, not saving");
1743                        }
1744                        return;
1745                    }
1746                    ensureThreadIdIfNeeded(conv, isStopping);
1747                    conv.setDraftState(true);
1748                    updateDraftSmsMessage(conv, contents);
1749                } finally {
1750                    DraftCache.getInstance().setSavingDraft(false);
1751                }
1752            }
1753        }, "WorkingMessage.asyncUpdateDraftSmsMessage").start();
1754    }
1755
1756    private void updateDraftSmsMessage(final Conversation conv, String contents) {
1757        final long threadId = conv.getThreadId();
1758        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1759            LogTag.debug("updateDraftSmsMessage tid=%d, contents=\"%s\"", threadId, contents);
1760        }
1761
1762        // If we don't have a valid thread, there's nothing to do.
1763        if (threadId <= 0) {
1764            return;
1765        }
1766
1767        ContentValues values = new ContentValues(3);
1768        values.put(Sms.THREAD_ID, threadId);
1769        values.put(Sms.BODY, contents);
1770        values.put(Sms.TYPE, Sms.MESSAGE_TYPE_DRAFT);
1771        SqliteWrapper.insert(mActivity, mContentResolver, Sms.CONTENT_URI, values);
1772        asyncDeleteDraftMmsMessage(conv);
1773        mMessageUri = null;
1774    }
1775
1776    private void asyncDelete(final Uri uri, final String selection, final String[] selectionArgs) {
1777        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1778            LogTag.debug("asyncDelete %s where %s", uri, selection);
1779        }
1780        new Thread(new Runnable() {
1781            @Override
1782            public void run() {
1783                SqliteWrapper.delete(mActivity, mContentResolver, uri, selection, selectionArgs);
1784            }
1785        }, "WorkingMessage.asyncDelete").start();
1786    }
1787
1788    public void asyncDeleteDraftSmsMessage(Conversation conv) {
1789        mHasSmsDraft = false;
1790
1791        final long threadId = conv.getThreadId();
1792        if (threadId > 0) {
1793            asyncDelete(ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, threadId),
1794                SMS_DRAFT_WHERE, null);
1795        }
1796    }
1797
1798    private void deleteDraftSmsMessage(long threadId) {
1799        SqliteWrapper.delete(mActivity, mContentResolver,
1800                ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, threadId),
1801                SMS_DRAFT_WHERE, null);
1802    }
1803
1804    private void asyncDeleteDraftMmsMessage(Conversation conv) {
1805        mHasMmsDraft = false;
1806
1807        final long threadId = conv.getThreadId();
1808        // If the thread id is < 1, then the thread_id in the pdu will be "" or NULL. We have
1809        // to clear those messages as well as ones with a valid thread id.
1810        final String where = Mms.THREAD_ID +  (threadId > 0 ? " = " + threadId : " IS NULL");
1811        asyncDelete(Mms.Draft.CONTENT_URI, where, null);
1812    }
1813
1814    /**
1815     * Ensure the thread id in conversation if needed, when we try to save a draft with a orphaned
1816     * one.
1817     * @param conv The conversation we are in.
1818     * @param isStopping Whether we are saving the draft in CMA'a onStop
1819     */
1820    private void ensureThreadIdIfNeeded(final Conversation conv, final boolean isStopping) {
1821        if (isStopping && conv.getMessageCount() == 0) {
1822            // We need to save the drafts in an unorphaned thread id. When the user goes
1823            // back to ConversationList while we're saving a draft from CMA's.onStop,
1824            // ConversationList will delete all threads from the thread table that
1825            // don't have associated sms or pdu entries. In case our thread got deleted,
1826            // well call clearThreadId() so ensureThreadId will query the db for the new
1827            // thread.
1828            conv.clearThreadId();   // force us to get the updated thread id
1829        }
1830        if (!conv.getRecipients().isEmpty()) {
1831            conv.ensureThreadId();
1832        }
1833    }
1834}
1835