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 
17 package com.android.mms.data;
18 
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.util.ArrayList;
22 import java.util.Arrays;
23 import java.util.HashMap;
24 import java.util.Iterator;
25 import java.util.List;
26 import java.util.Set;
27 
28 import android.app.Activity;
29 import android.content.ContentResolver;
30 import android.content.ContentUris;
31 import android.content.ContentValues;
32 import android.content.Context;
33 import android.database.Cursor;
34 import android.database.sqlite.SqliteWrapper;
35 import android.net.Uri;
36 import android.os.AsyncTask;
37 import android.os.Bundle;
38 import android.provider.Telephony.Mms;
39 import android.provider.Telephony.MmsSms;
40 import android.provider.Telephony.MmsSms.PendingMessages;
41 import android.provider.Telephony.Sms;
42 import android.telephony.SmsMessage;
43 import android.text.TextUtils;
44 import android.util.Log;
45 import android.util.Pair;
46 
47 import com.android.common.contacts.DataUsageStatUpdater;
48 import com.android.common.userhappiness.UserHappinessSignals;
49 import com.android.mms.ContentRestrictionException;
50 import com.android.mms.ExceedMessageSizeException;
51 import com.android.mms.LogTag;
52 import com.android.mms.MmsApp;
53 import com.android.mms.MmsConfig;
54 import com.android.mms.ResolutionException;
55 import com.android.mms.UnsupportContentTypeException;
56 import com.android.mms.model.ImageModel;
57 import com.android.mms.model.SlideModel;
58 import com.android.mms.model.SlideshowModel;
59 import com.android.mms.model.TextModel;
60 import com.android.mms.transaction.MessageSender;
61 import com.android.mms.transaction.MmsMessageSender;
62 import com.android.mms.transaction.SmsMessageSender;
63 import com.android.mms.ui.ComposeMessageActivity;
64 import com.android.mms.ui.MessageUtils;
65 import com.android.mms.ui.MessagingPreferenceActivity;
66 import com.android.mms.ui.SlideshowEditor;
67 import com.android.mms.util.DraftCache;
68 import com.android.mms.util.Recycler;
69 import com.android.mms.util.ThumbnailManager;
70 import com.android.mms.widget.MmsWidgetProvider;
71 import com.google.android.mms.ContentType;
72 import com.google.android.mms.MmsException;
73 import com.google.android.mms.pdu.EncodedStringValue;
74 import com.google.android.mms.pdu.PduBody;
75 import com.google.android.mms.pdu.PduHeaders;
76 import com.google.android.mms.pdu.PduPersister;
77 import com.google.android.mms.pdu.SendReq;
78 
79 /**
80  * Contains all state related to a message being edited by the user.
81  */
82 public 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          */
onProtocolChanged(boolean mms)173         void onProtocolChanged(boolean mms);
174 
175         /**
176          * Called when an attachment on the message has changed.
177          */
onAttachmentChanged()178         void onAttachmentChanged();
179 
180         /**
181          * Called just before the process of sending a message.
182          */
onPreMessageSent()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          */
onMessageSent()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          */
onMaxPendingMessagesReached()196         void onMaxPendingMessagesReached();
197 
198         /**
199          * Called if there's an attachment error while resizing the images just before sending.
200          */
onAttachmentError(int error)201         void onAttachmentError(int error);
202     }
203 
WorkingMessage(ComposeMessageActivity activity)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      */
createEmpty(ComposeMessageActivity activity)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      */
load(ComposeMessageActivity activity, Uri uri)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 
correctAttachmentState(boolean showToast)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 
loadFromUri(Uri uri)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      */
loadDraft(ComposeMessageActivity activity, final Conversation conv, final Runnable onDraftLoaded)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      */
setText(CharSequence s)358     public void setText(CharSequence s) {
359         mText = s;
360     }
361 
362     /**
363      * Returns the current message text.
364      */
getText()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      */
hasText()373     public boolean hasText() {
374         return mText != null && TextUtils.getTrimmedLength(mText) > 0;
375     }
376 
removeAttachment(boolean notify)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 
removeThumbnailsFromCache(SlideshowModel slideshow)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      */
setAttachment(int type, Uri dataUri, boolean append)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      */
isWorthSaving()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 
cancelThumbnailLoading()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      */
isFakeMmsForDraft()536     public boolean isFakeMmsForDraft() {
537         return (mMmsState & FORCE_MMS) > 0;
538     }
539 
540     /**
541      * Makes sure mSlideshow is set up.
542      */
ensureSlideshow()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      */
changeMedia(int type, Uri uri, SlideshowEditor slideShowEditor)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      */
appendMedia(int type, Uri uri, SlideshowEditor slideShowEditor)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 
internalChangeMedia(int type, Uri uri, int slideNum, SlideshowEditor slideShowEditor)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      */
hasAttachment()664     public boolean hasAttachment() {
665         return (mAttachmentType > TEXT);
666     }
667 
668     /**
669      * Returns the slideshow associated with this message.
670      */
getSlideshow()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      */
hasSlideshow()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      */
setSubject(CharSequence s, boolean notify)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      */
getSubject()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      */
hasSubject()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      */
syncTextToSlideshow()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      */
syncTextFromSlideshow()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      */
removeSubjectIfEmpty(boolean notify)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      */
prepareForSave(boolean notify)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      */
syncWorkingRecipients()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 
getWorkingRecipients()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.
removeFakeMmsForDraft()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      */
saveAsMms(boolean notify)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      */
saveDraft(final boolean isStopping)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 
discard()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 
unDiscard()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      */
isDiscarded()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      */
writeStateToBundle(Bundle bundle)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      */
readStateFromBundle(Bundle bundle)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      */
setWorkingRecipients(List<String> numbers)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 
dumpWorkingRecipients()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 
dump()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      */
setConversation(Conversation conv)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 
getConversation()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      */
setHasEmail(boolean hasEmail, boolean notify)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      */
setHasMultipleRecipients(boolean hasMultipleRecipients, boolean notify)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      */
requiresMms()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      */
hasMmsContentToSave()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     */
setLengthRequiresMms(boolean mmsRequired, boolean notify)1098     public void setLengthRequiresMms(boolean mmsRequired, boolean notify) {
1099         updateState(LENGTH_REQUIRES_MMS, mmsRequired, notify);
1100     }
1101 
stateString(int state)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      */
updateState(int state, boolean on, boolean notify)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      */
send(final String recipientsInUI)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.
updateSendStats(final Conversation conv)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 
addressContainsEmailToMms(Conversation conv, String text)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 
preSendSmsWorker(Conversation conv, String msgText, String recipientsInUI)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 
sendSmsWorker(String msgText, String semiSepRecipients, long threadId)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 
sendMmsWorker(Conversation conv, Uri mmsUri, PduPersister persister, SlideshowModel slideshow, SendReq sendReq, boolean textOnly)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 
markMmsMessageWithError(Uri mmsUri)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 
readDraftMmsMessage(Context context, Conversation conv, StringBuilder sb)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      */
makeSendReq(Conversation conv, CharSequence subject)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 
createDraftMmsMessage(PduPersister persister, SendReq sendReq, SlideshowModel slideshow, Uri preUri, Context context, HashMap<Uri, InputStream> preOpenedFiles)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 
asyncUpdateDraftMmsMessage(final Conversation conv, final boolean isStopping)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 
updateDraftMmsMessage(Uri uri, PduPersister persister, SlideshowModel slideshow, SendReq sendReq, HashMap<Uri, InputStream> preOpenedFiles)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 
closePreOpenedFiles(HashMap<Uri, InputStream> preOpenedFiles)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      */
readDraftSmsMessage(Conversation conv)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 
clearConversation(final Conversation conv, boolean resetThreadId)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 
asyncUpdateDraftSmsMessage(final Conversation conv, final String contents, final boolean isStopping)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 
updateDraftSmsMessage(final Conversation conv, String contents)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 
asyncDelete(final Uri uri, final String selection, final String[] selectionArgs)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 
asyncDeleteDraftSmsMessage(Conversation conv)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 
deleteDraftSmsMessage(long threadId)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 
asyncDeleteDraftMmsMessage(Conversation conv)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      */
ensureThreadIdIfNeeded(final Conversation conv, final boolean isStopping)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