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